Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/aidotnet_codexswitch.json
T

1 line
351 KiB
JSON

{"repo": "aidotnet/codexswitch", "files": {"CodexSwitch.Tests/BuiltInCatalogMigrationTests.cs": {"ab93a928c55fc0df679494e185648c1b7e93276d": {"t": 1785746200.8055158, "n": 29189, "h": "75a4b51e788f51a7", "c": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CodexSwitch.Models;\nusing CodexSwitch.Proxy;\nusing CodexSwitch.Services;\n\nnamespace CodexSwitch.Tests;\n\npublic sealed class BuiltInCatalogMigrationTests\n{\n private static readonly JsonSerializerOptions JsonOptions = new()\n {\n WriteIndented = true,\n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n Converters = { new JsonStringEnumConverter() }\n };\n\n [Fact]\n public void LoadPricing_UpgradesBuiltInCatalogToOfficialDefaults()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new ModelPricingCatalog\n {\n SchemaVersion = \"1.0\",\n Currency = \"USD\",\n BillingUnitTokens = 1_000_000,\n FastMode =\n {\n DefaultMultiplier = 2m,\n ModelOverrides =\n {\n [\"gpt-5.5\"] = 2.5m\n }\n },\n Models =\n {\n new ModelPricingRule\n {\n Id = \"gpt-5.5\",\n DisplayName = \"GPT-5.5\",\n IconSlug = \"openai\",\n Input = FlatTable(1.25m),\n CachedInput = FlatTable(0.125m),\n Output = FlatTable(10m)\n }\n }\n };\n\n WriteJson(paths.PricingPath, legacy);\n\n var upgraded = store.LoadPricing();\n\n Assert.Equal(BuiltInModelCatalog.PricingSchemaVersion, upgraded.SchemaVersion);\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4-mini\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.3-codex\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-opus-4-7\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-3-5-sonnet\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5\");\n\n var gpt55 = Assert.Single(upgraded.Models, rule => rule.Id == \"gpt-5.5\");\n Assert.Equal(5m, gpt55.Input.Tiers[0].PricePerUnit);\n Assert.Equal(BuiltInModelCatalog.OpenAiLongContextThresholdTokens, gpt55.Input.Tiers[0].UpToTokens);\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5.5*\"));\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5\"));\n\n var deepSeekFlash = Assert.Single(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(\"deepseek-chat\", deepSeekFlash.Aliases);\n Assert.Contains(\"deepseek-reasoner\", deepSeekFlash.Aliases);\n\n var mimoPro = Assert.Single(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"mimo-v2-pro\", mimoPro.Aliases);\n Assert.Equal(BuiltInModelCatalog.XiaomiLongContextThresholdTokens, mimoPro.Input.Tiers[0].UpToTokens);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_ExpandsBuiltInProviderModelLists()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"openai-official\",\n BuiltinId = ProviderTemplateCatalog.OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n Models =\n {\n new ModelRouteConfig { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }\n }\n },\n new ProviderConfig\n {\n Id = \"anthropic\",\n BuiltinId = ProviderTemplateCatalog.AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n Models =\n {\n new ModelRouteConfig { Id = \"claude-sonnet-4-5\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var openAi = Assert.Single(upgraded.Providers, provider => provider.Id == \"openai-official\");\n var anthropic = Assert.Single(upgraded.Providers, provider => provider.Id == \"anthropic\");\n var deepSeek = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n var xiaomi = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.XiaomiBuiltinId, StringComparison.OrdinalIgnoreCase));\n\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4-mini\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.3-codex\");\n AssertDefaultConversion(openAi);\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-opus-4-7\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-3-5-sonnet\");\n AssertDefaultConversion(anthropic);\n Assert.Equal(\"https://api.deepseek.com/v1\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-v4-flash\");\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-reasoner\");\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n AssertDefaultConversion(deepSeek);\n Assert.Equal(ProviderTemplateCatalog.OpenAiOfficialBaseUrl, xiaomi.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, xiaomi.Protocol);\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2-flash\");\n AssertDefaultConversion(xiaomi);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void DeepSeekTemplate_UsesOpenAiChatEndpointAndRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, []);\n\n Assert.Equal(\"https://api.deepseek.com/v1\", provider.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, provider.Protocol);\n Assert.True(provider.SupportsClaudeCode);\n Assert.All(provider.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n }\n\n [Fact]\n public void XiaomiTemplate_UsesOpenAiEndpointAndRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, []);\n\n Assert.Equal(ProviderTemplateCatalog.OpenAiOfficialBaseUrl, provider.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, provider.Protocol);\n Assert.Contains(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"gpt-5.4\");\n AssertDefaultConversion(provider);\n }\n\n [Fact]\n public void EnsureValidDefaults_PreservesXiaomiTemplateWhenUsingOpenAiEndpoint()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, []);\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n Assert.Equal(ProviderTemplateCatalog.XiaomiBuiltinId, provider.BuiltinId);\n Assert.Equal(ProviderTemplateCatalog.OpenAiOfficialBaseUrl, provider.BaseUrl);\n Assert.Equal(\"mimo-v2.5-pro\", provider.DefaultModel);\n Assert.Contains(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"gpt-5.4\");\n }\n\n [Fact]\n public void EnsureValidDefaults_MigratesLegacyXiaomiEndpointToOpenAiEndpoint()\n {\n var provider = new ProviderConfig\n {\n Id = \"xiaomi-mimo\",\n BuiltinId = ProviderTemplateCatalog.XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n BaseUrl = ProviderTemplateCatalog.XiaomiLegacyBaseUrl,\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\"\n };\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n Assert.Equal(ProviderTemplateCatalog.OpenAiOfficialBaseUrl, provider.BaseUrl);\n Assert.Equal(ProviderTemplateCatalog.XiaomiBuiltinId, provider.BuiltinId);\n Assert.Contains(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"gpt-5.4\");\n }\n\n [Fact]\n public void CodexOAuthTemplate_EnablesWebSocketsByDefault()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.CodexOAuthBuiltinId, []);\n\n Assert.True(provider.SupportsWebSockets == true);\n }\n\n [Fact]\n public void LoadConfig_MigratesCodexOAuthWebSocketDefaultButPreservesExplicitFalse()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var legacy = new AppConfig\n {\n ActiveProviderId = \"codex-oauth\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"codex-oauth\",\n BuiltinId = ProviderTemplateCatalog.CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n BaseUrl = ProviderTemplateCatalog.CodexOAuthTemplate.BaseUrl,\n AuthMode = ProviderAuthMode.OAuth,\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = ProviderTemplateCatalog.CodexOAuthTemplate.DefaultModel\n },\n new ProviderConfig\n {\n Id = \"codex-oauth-disabled\",\n BuiltinId = ProviderTemplateCatalog.CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth Disabled\",\n BaseUrl = ProviderTemplateCatalog.CodexOAuthTemplate.BaseUrl,\n AuthMode = ProviderAuthMode.OAuth,\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = ProviderTemplateCatalog.CodexOAuthTemplate.DefaultModel,\n SupportsWebSockets = false\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n\n Assert.True(upgraded.Providers.Single(provider => provider.Id == \"codex-oauth\").SupportsWebSockets == true);\n Assert.False(upgraded.Providers.Single(provider => provider.Id == \"codex-oauth-disabled\").SupportsWebSockets == true);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_MigratesLegacyDeepSeekBuiltinToOpenAiChat()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"deepseek\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"deepseek\",\n BuiltinId = ProviderTemplateCatalog.DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n BaseUrl = \"https://api.deepseek.com/anthropic\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"deepseek-chat\",\n Models =\n {\n new ModelRouteConfig { Id = \"deepseek-chat\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var deepSeek = Assert.Single(upgraded.Providers, provider => provider.Id == \"deepseek\");\n\n Assert.Equal(ProviderTemplateCatalog.DeepSeekBuiltinId, deepSeek.BuiltinId);\n Assert.Equal(\"https://api.deepseek.com/v1\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Equal(\"deepseek-chat\", deepSeek.DefaultModel);\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void RoutinAiTemplate_IncludesRequestedDeepSeekAndMimoRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n\n var deepSeekFlash = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-flash\");\n var deepSeekPro = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-pro\");\n var mimoFlash = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-flash\");\n var mimoV2Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-pro\");\n var mimoV25Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeekFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeekPro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV2Pro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV25Pro.Protocol);\n Assert.Equal(\"priority\", deepSeekFlash.ServiceTier);\n Assert.Equal(\"priority\", deepSeekPro.ServiceTier);\n Assert.Equal(\"priority\", mimoFlash.ServiceTier);\n Assert.Equal(\"priority\", mimoV2Pro.ServiceTier);\n Assert.Equal(\"priority\", mimoV25Pro.ServiceTier);\n var deepSeekFlashCost = deepSeekFlash.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Flash cost settings should be seeded.\");\n var deepSeekProCost = deepSeekPro.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Pro cost settings should be seeded.\");\n var mimoFlashCost = mimoFlash.Cost ?? throw new InvalidOperationException(\"MiMo V2 Flash cost settings should be seeded.\");\n var mimoV2ProCost = mimoV2Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2 Pro cost settings should be seeded.\");\n var mimoV25ProCost = mimoV25Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2.5 Pro cost settings should be seeded.\");\n Assert.True(deepSeekFlashCost.FastMode);\n Assert.True(deepSeekProCost.FastMode);\n Assert.True(mimoFlashCost.FastMode);\n Assert.True(mimoV2ProCost.FastMode);\n Assert.True(mimoV25ProCost.FastMode);\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-chat\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-reasoner\");\n }\n\n [Fact]\n public void EnsureValidDefaults_MigratesRoutinAiDeepSeekRoutesToOpenAiChat()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n foreach (var route in provider.Models.Where(route => route.Id.StartsWith(\"deepseek-\", StringComparison.OrdinalIgnoreCase)))\n route.Protocol = ProviderProtocol.OpenAiResponses;\n\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n Assert.Equal(\n ProviderProtocol.OpenAiChat,\n provider.Models.Single(route => route.Id == \"deepseek-v4-flash\").Protocol);\n Assert.Equal(\n ProviderProtocol.OpenAiChat,\n provider.Models.Single(route => route.Id == \"deepseek-v4-pro\").Protocol);\n }\n\n [Fact]\n public void SaveConfig_PreservesEditedBuiltInProviderModelRouteFields()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n var route = provider.Models.Single(model => model.Id == \"gpt-5\");\n route.DisplayName = \"Custom GPT\";\n route.Protocol = ProviderProtocol.OpenAiChat;\n route.UpstreamModel = null;\n route.ServiceTier = null;\n route.Cost = new ProviderCostSettings { FastMode = false };\n\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n store.SaveConfig(config);\n var reloaded = store.LoadConfig();\n var reloadedProvider = Assert.Single(reloaded.Providers, item => item.Id == provider.Id);\n var reloadedRoute = reloadedProvider.Models.Single(model => model.Id == \"gpt-5\");\n\n Assert.Equal(\"Custom GPT\", reloadedRoute.DisplayName);\n Assert.Equal(ProviderProtocol.OpenAiChat, reloadedRoute.Protocol);\n Assert.Null(reloadedRoute.UpstreamModel);\n Assert.Null(reloadedRoute.ServiceTier);\n var cost = Assert.IsType<ProviderCostSettings>(reloadedRoute.Cost);\n Assert.False(cost.FastMode);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesActiveProviderForRequestedModel()\n {\n var config = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, [\"openai-official\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, [\"openai-official\", \"anthropic\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, [\"openai-official\", \"anthropic\", \"deepseek\"])\n }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, \"claude-sonnet-4-5\");\n\n Assert.NotNull(selection);\n Assert.Equal(\"openai-official\", selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n\n var deepSeekSelection = ProviderRoutingResolver.Resolve(config, \"deepseek-reasoner\");\n Assert.NotNull(deepSeekSelection);\n Assert.Equal(\"openai-official\", deepSeekSelection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, deepSeekSelection.Model?.Id);\n\n var xiaomiSelection = ProviderRoutingResolver.Resolve(config, \"mimo-v2-pro\");\n Assert.NotNull(xiaomiSelection);\n Assert.Equal(\"openai-official\", xiaomiSelection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, xiaomiSelection.Model?.Id);\n\n var listings = ProviderRoutingResolver.CollectModelListings(config);\n var gpt54 = Assert.Single(listings, item => item.Id == \"gpt-5.4\");\n var deepSeekFlash = Assert.Single(listings, item => item.Id == \"deepseek-v4-flash\");\n var mimoPro = Assert.Single(listings, item => item.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"openai-official\", gpt54.ProviderIds);\n Assert.Contains(\"deepseek\", deepSeekFlash.ProviderIds);\n Assert.Contains(\"xiaomi-mimo\", mimoPro.ProviderIds);\n }\n\n [Fact]\n public void LoadConfig_DoesNotRestoreDeletedBuiltInProvider()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n ActiveCodexProviderId = \"openai-official\",\n DeletedBuiltinProviderIds = { ProviderTemplateCatalog.DeepSeekBuiltinId },\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, [])\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var loaded = store.LoadConfig();\n\n Assert.DoesNotContain(loaded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n Assert.Contains(loaded.DeletedBuiltinProviderIds, id =>\n string.Equals(id, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesDefaultConversionForActiveProvider()\n {\n foreach (var templateId in new[]\n {\n ProviderTemplateCatalog.AnthropicBuiltinId,\n ProviderTemplateCatalog.DeepSeekBuiltinId,\n ProviderTemplateCatalog.XiaomiBuiltinId\n })\n {\n var openAi = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []);\n var provider = ProviderTemplateCatalog.CreateProvider(templateId, [openAi.Id]);\n var expectedRoute = provider.Models.FirstOrDefault(model =>\n string.Equals(model.Id, provider.DefaultModel, StringComparison.OrdinalIgnoreCase));\n var expectedUpstream = string.IsNullOrWhiteSpace(expectedRoute?.UpstreamModel)\n ? provider.DefaultModel\n : expectedRoute.UpstreamModel;\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { openAi, provider }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.NotNull(selection);\n Assert.Equal(provider.Id, selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n Assert.Equal(expectedUpstream, selection.Model?.UpstreamModel);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_IgnoresDisabledDefaultConversion()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n conversion.Enabled = false;\n\n Assert.False(ProviderRoutingResolver.ProviderSupports(provider, [CodexSwitchDefaults.ManagedCodexModel]));\n\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n Assert.DoesNotContain(listings, listing =>\n string.Equals(listing.Id, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase));\n }\n\n [Fact]\n public void ProviderRoutingResolver_ListsEnabledConversionSources()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n var listing = Assert.Single(listings, item => item.Id == CodexSwitchDefaults.ManagedCodexModel);\n Assert.Contains(provider.Id, listing.ProviderIds);\n }\n\n [Fact]\n public void EnsureValidDefaults_SeedsAndPreservesDefaultModelConversions()\n {\n var disabledProvider = new ProviderConfig\n {\n Id = \"disabled\",\n DisplayName = \"Disabled\",\n BaseUrl = \"https://example.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-custom\",\n ModelConversions =\n {\n new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = false\n }\n }\n };\n var config = new AppConfig\n {\n ActiveProviderId = disabledProvider.Id,\n Providers = { disabledProvider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n var provider = Assert.Single(config.Providers, item => item.Id == \"disabled\");\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.False(conversion.Enabled);\n\n conversion.Enabled = true;\n provider.DefaultModel = \"claude-new-default\";\n var selection = ProviderRoutingResolver.Resolve(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n }, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.Equal(\"claude-new-default\", selection?.Model?.UpstreamModel);\n }\n\n [Fact]\n public void IconCacheService_UsesOfficialXiaomiFallbackIconUrl()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n using var httpClient = new HttpClient();\n var icons = new IconCacheService(paths, httpClient);\n\n Assert.Equal(\n \"https://platform.xiaomimimo.com/static/favicon.874c9507.png\",\n icons.GetIconUrl(\"xiaomi\"));\n Assert.Equal(\"xiaomi\", IconCacheService.ResolveModelIconSlug(\"mimo-v2.5-pro\"));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n private static TokenPriceTable FlatTable(decimal price)\n {\n var table = new TokenPriceTable();\n table.Tiers.Add(new PricingTier { UpToTokens = null, PricePerUnit = price });\n return table;\n }\n\n private static void WriteJson<T>(string path, T value)\n {\n Directory.CreateDirectory(Path.GetDirectoryName(path)!);\n File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions));\n }\n\n private static void AssertDefaultConversion(ProviderConfig provider)\n {\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.True(conversion.Enabled);\n Assert.True(conversion.UseDefaultModel);\n Assert.Null(conversion.TargetModel);\n }\n\n private static string CreateTempDirectory()\n {\n var path = Path.Combine(Path.GetTempPath(), \"CodexSwitchTests\", Guid.NewGuid().ToString(\"N\"));\n Directory.CreateDirectory(path);\n return path;\n }\n}\n"}, "c840982149c2ae4e42ba5340408660b51a700005": {"t": 1785760552.9546962, "n": 29189, "h": "75a4b51e788f51a7", "c": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CodexSwitch.Models;\nusing CodexSwitch.Proxy;\nusing CodexSwitch.Services;\n\nnamespace CodexSwitch.Tests;\n\npublic sealed class BuiltInCatalogMigrationTests\n{\n private static readonly JsonSerializerOptions JsonOptions = new()\n {\n WriteIndented = true,\n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n Converters = { new JsonStringEnumConverter() }\n };\n\n [Fact]\n public void LoadPricing_UpgradesBuiltInCatalogToOfficialDefaults()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new ModelPricingCatalog\n {\n SchemaVersion = \"1.0\",\n Currency = \"USD\",\n BillingUnitTokens = 1_000_000,\n FastMode =\n {\n DefaultMultiplier = 2m,\n ModelOverrides =\n {\n [\"gpt-5.5\"] = 2.5m\n }\n },\n Models =\n {\n new ModelPricingRule\n {\n Id = \"gpt-5.5\",\n DisplayName = \"GPT-5.5\",\n IconSlug = \"openai\",\n Input = FlatTable(1.25m),\n CachedInput = FlatTable(0.125m),\n Output = FlatTable(10m)\n }\n }\n };\n\n WriteJson(paths.PricingPath, legacy);\n\n var upgraded = store.LoadPricing();\n\n Assert.Equal(BuiltInModelCatalog.PricingSchemaVersion, upgraded.SchemaVersion);\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4-mini\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.3-codex\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-opus-4-7\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-3-5-sonnet\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5\");\n\n var gpt55 = Assert.Single(upgraded.Models, rule => rule.Id == \"gpt-5.5\");\n Assert.Equal(5m, gpt55.Input.Tiers[0].PricePerUnit);\n Assert.Equal(BuiltInModelCatalog.OpenAiLongContextThresholdTokens, gpt55.Input.Tiers[0].UpToTokens);\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5.5*\"));\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5\"));\n\n var deepSeekFlash = Assert.Single(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(\"deepseek-chat\", deepSeekFlash.Aliases);\n Assert.Contains(\"deepseek-reasoner\", deepSeekFlash.Aliases);\n\n var mimoPro = Assert.Single(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"mimo-v2-pro\", mimoPro.Aliases);\n Assert.Equal(BuiltInModelCatalog.XiaomiLongContextThresholdTokens, mimoPro.Input.Tiers[0].UpToTokens);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_ExpandsBuiltInProviderModelLists()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"openai-official\",\n BuiltinId = ProviderTemplateCatalog.OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n Models =\n {\n new ModelRouteConfig { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }\n }\n },\n new ProviderConfig\n {\n Id = \"anthropic\",\n BuiltinId = ProviderTemplateCatalog.AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n Models =\n {\n new ModelRouteConfig { Id = \"claude-sonnet-4-5\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var openAi = Assert.Single(upgraded.Providers, provider => provider.Id == \"openai-official\");\n var anthropic = Assert.Single(upgraded.Providers, provider => provider.Id == \"anthropic\");\n var deepSeek = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n var xiaomi = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.XiaomiBuiltinId, StringComparison.OrdinalIgnoreCase));\n\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4-mini\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.3-codex\");\n AssertDefaultConversion(openAi);\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-opus-4-7\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-3-5-sonnet\");\n AssertDefaultConversion(anthropic);\n Assert.Equal(\"https://api.deepseek.com/v1\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-v4-flash\");\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-reasoner\");\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n AssertDefaultConversion(deepSeek);\n Assert.Equal(ProviderTemplateCatalog.OpenAiOfficialBaseUrl, xiaomi.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, xiaomi.Protocol);\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2-flash\");\n AssertDefaultConversion(xiaomi);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void DeepSeekTemplate_UsesOpenAiChatEndpointAndRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, []);\n\n Assert.Equal(\"https://api.deepseek.com/v1\", provider.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, provider.Protocol);\n Assert.True(provider.SupportsClaudeCode);\n Assert.All(provider.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n }\n\n [Fact]\n public void XiaomiTemplate_UsesOpenAiEndpointAndRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, []);\n\n Assert.Equal(ProviderTemplateCatalog.OpenAiOfficialBaseUrl, provider.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, provider.Protocol);\n Assert.Contains(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"gpt-5.4\");\n AssertDefaultConversion(provider);\n }\n\n [Fact]\n public void EnsureValidDefaults_PreservesXiaomiTemplateWhenUsingOpenAiEndpoint()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, []);\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n Assert.Equal(ProviderTemplateCatalog.XiaomiBuiltinId, provider.BuiltinId);\n Assert.Equal(ProviderTemplateCatalog.OpenAiOfficialBaseUrl, provider.BaseUrl);\n Assert.Equal(\"mimo-v2.5-pro\", provider.DefaultModel);\n Assert.Contains(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"gpt-5.4\");\n }\n\n [Fact]\n public void EnsureValidDefaults_MigratesLegacyXiaomiEndpointToOpenAiEndpoint()\n {\n var provider = new ProviderConfig\n {\n Id = \"xiaomi-mimo\",\n BuiltinId = ProviderTemplateCatalog.XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n BaseUrl = ProviderTemplateCatalog.XiaomiLegacyBaseUrl,\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\"\n };\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n Assert.Equal(ProviderTemplateCatalog.OpenAiOfficialBaseUrl, provider.BaseUrl);\n Assert.Equal(ProviderTemplateCatalog.XiaomiBuiltinId, provider.BuiltinId);\n Assert.Contains(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"gpt-5.4\");\n }\n\n [Fact]\n public void CodexOAuthTemplate_EnablesWebSocketsByDefault()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.CodexOAuthBuiltinId, []);\n\n Assert.True(provider.SupportsWebSockets == true);\n }\n\n [Fact]\n public void LoadConfig_MigratesCodexOAuthWebSocketDefaultButPreservesExplicitFalse()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var legacy = new AppConfig\n {\n ActiveProviderId = \"codex-oauth\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"codex-oauth\",\n BuiltinId = ProviderTemplateCatalog.CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n BaseUrl = ProviderTemplateCatalog.CodexOAuthTemplate.BaseUrl,\n AuthMode = ProviderAuthMode.OAuth,\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = ProviderTemplateCatalog.CodexOAuthTemplate.DefaultModel\n },\n new ProviderConfig\n {\n Id = \"codex-oauth-disabled\",\n BuiltinId = ProviderTemplateCatalog.CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth Disabled\",\n BaseUrl = ProviderTemplateCatalog.CodexOAuthTemplate.BaseUrl,\n AuthMode = ProviderAuthMode.OAuth,\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = ProviderTemplateCatalog.CodexOAuthTemplate.DefaultModel,\n SupportsWebSockets = false\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n\n Assert.True(upgraded.Providers.Single(provider => provider.Id == \"codex-oauth\").SupportsWebSockets == true);\n Assert.False(upgraded.Providers.Single(provider => provider.Id == \"codex-oauth-disabled\").SupportsWebSockets == true);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_MigratesLegacyDeepSeekBuiltinToOpenAiChat()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"deepseek\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"deepseek\",\n BuiltinId = ProviderTemplateCatalog.DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n BaseUrl = \"https://api.deepseek.com/anthropic\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"deepseek-chat\",\n Models =\n {\n new ModelRouteConfig { Id = \"deepseek-chat\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var deepSeek = Assert.Single(upgraded.Providers, provider => provider.Id == \"deepseek\");\n\n Assert.Equal(ProviderTemplateCatalog.DeepSeekBuiltinId, deepSeek.BuiltinId);\n Assert.Equal(\"https://api.deepseek.com/v1\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Equal(\"deepseek-chat\", deepSeek.DefaultModel);\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void RoutinAiTemplate_IncludesRequestedDeepSeekAndMimoRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n\n var deepSeekFlash = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-flash\");\n var deepSeekPro = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-pro\");\n var mimoFlash = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-flash\");\n var mimoV2Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-pro\");\n var mimoV25Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeekFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeekPro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV2Pro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV25Pro.Protocol);\n Assert.Equal(\"priority\", deepSeekFlash.ServiceTier);\n Assert.Equal(\"priority\", deepSeekPro.ServiceTier);\n Assert.Equal(\"priority\", mimoFlash.ServiceTier);\n Assert.Equal(\"priority\", mimoV2Pro.ServiceTier);\n Assert.Equal(\"priority\", mimoV25Pro.ServiceTier);\n var deepSeekFlashCost = deepSeekFlash.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Flash cost settings should be seeded.\");\n var deepSeekProCost = deepSeekPro.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Pro cost settings should be seeded.\");\n var mimoFlashCost = mimoFlash.Cost ?? throw new InvalidOperationException(\"MiMo V2 Flash cost settings should be seeded.\");\n var mimoV2ProCost = mimoV2Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2 Pro cost settings should be seeded.\");\n var mimoV25ProCost = mimoV25Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2.5 Pro cost settings should be seeded.\");\n Assert.True(deepSeekFlashCost.FastMode);\n Assert.True(deepSeekProCost.FastMode);\n Assert.True(mimoFlashCost.FastMode);\n Assert.True(mimoV2ProCost.FastMode);\n Assert.True(mimoV25ProCost.FastMode);\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-chat\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-reasoner\");\n }\n\n [Fact]\n public void EnsureValidDefaults_MigratesRoutinAiDeepSeekRoutesToOpenAiChat()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n foreach (var route in provider.Models.Where(route => route.Id.StartsWith(\"deepseek-\", StringComparison.OrdinalIgnoreCase)))\n route.Protocol = ProviderProtocol.OpenAiResponses;\n\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n Assert.Equal(\n ProviderProtocol.OpenAiChat,\n provider.Models.Single(route => route.Id == \"deepseek-v4-flash\").Protocol);\n Assert.Equal(\n ProviderProtocol.OpenAiChat,\n provider.Models.Single(route => route.Id == \"deepseek-v4-pro\").Protocol);\n }\n\n [Fact]\n public void SaveConfig_PreservesEditedBuiltInProviderModelRouteFields()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n var route = provider.Models.Single(model => model.Id == \"gpt-5\");\n route.DisplayName = \"Custom GPT\";\n route.Protocol = ProviderProtocol.OpenAiChat;\n route.UpstreamModel = null;\n route.ServiceTier = null;\n route.Cost = new ProviderCostSettings { FastMode = false };\n\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n store.SaveConfig(config);\n var reloaded = store.LoadConfig();\n var reloadedProvider = Assert.Single(reloaded.Providers, item => item.Id == provider.Id);\n var reloadedRoute = reloadedProvider.Models.Single(model => model.Id == \"gpt-5\");\n\n Assert.Equal(\"Custom GPT\", reloadedRoute.DisplayName);\n Assert.Equal(ProviderProtocol.OpenAiChat, reloadedRoute.Protocol);\n Assert.Null(reloadedRoute.UpstreamModel);\n Assert.Null(reloadedRoute.ServiceTier);\n var cost = Assert.IsType<ProviderCostSettings>(reloadedRoute.Cost);\n Assert.False(cost.FastMode);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesActiveProviderForRequestedModel()\n {\n var config = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, [\"openai-official\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, [\"openai-official\", \"anthropic\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, [\"openai-official\", \"anthropic\", \"deepseek\"])\n }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, \"claude-sonnet-4-5\");\n\n Assert.NotNull(selection);\n Assert.Equal(\"openai-official\", selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n\n var deepSeekSelection = ProviderRoutingResolver.Resolve(config, \"deepseek-reasoner\");\n Assert.NotNull(deepSeekSelection);\n Assert.Equal(\"openai-official\", deepSeekSelection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, deepSeekSelection.Model?.Id);\n\n var xiaomiSelection = ProviderRoutingResolver.Resolve(config, \"mimo-v2-pro\");\n Assert.NotNull(xiaomiSelection);\n Assert.Equal(\"openai-official\", xiaomiSelection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, xiaomiSelection.Model?.Id);\n\n var listings = ProviderRoutingResolver.CollectModelListings(config);\n var gpt54 = Assert.Single(listings, item => item.Id == \"gpt-5.4\");\n var deepSeekFlash = Assert.Single(listings, item => item.Id == \"deepseek-v4-flash\");\n var mimoPro = Assert.Single(listings, item => item.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"openai-official\", gpt54.ProviderIds);\n Assert.Contains(\"deepseek\", deepSeekFlash.ProviderIds);\n Assert.Contains(\"xiaomi-mimo\", mimoPro.ProviderIds);\n }\n\n [Fact]\n public void LoadConfig_DoesNotRestoreDeletedBuiltInProvider()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n ActiveCodexProviderId = \"openai-official\",\n DeletedBuiltinProviderIds = { ProviderTemplateCatalog.DeepSeekBuiltinId },\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, [])\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var loaded = store.LoadConfig();\n\n Assert.DoesNotContain(loaded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n Assert.Contains(loaded.DeletedBuiltinProviderIds, id =>\n string.Equals(id, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesDefaultConversionForActiveProvider()\n {\n foreach (var templateId in new[]\n {\n ProviderTemplateCatalog.AnthropicBuiltinId,\n ProviderTemplateCatalog.DeepSeekBuiltinId,\n ProviderTemplateCatalog.XiaomiBuiltinId\n })\n {\n var openAi = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []);\n var provider = ProviderTemplateCatalog.CreateProvider(templateId, [openAi.Id]);\n var expectedRoute = provider.Models.FirstOrDefault(model =>\n string.Equals(model.Id, provider.DefaultModel, StringComparison.OrdinalIgnoreCase));\n var expectedUpstream = string.IsNullOrWhiteSpace(expectedRoute?.UpstreamModel)\n ? provider.DefaultModel\n : expectedRoute.UpstreamModel;\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { openAi, provider }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.NotNull(selection);\n Assert.Equal(provider.Id, selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n Assert.Equal(expectedUpstream, selection.Model?.UpstreamModel);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_IgnoresDisabledDefaultConversion()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n conversion.Enabled = false;\n\n Assert.False(ProviderRoutingResolver.ProviderSupports(provider, [CodexSwitchDefaults.ManagedCodexModel]));\n\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n Assert.DoesNotContain(listings, listing =>\n string.Equals(listing.Id, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase));\n }\n\n [Fact]\n public void ProviderRoutingResolver_ListsEnabledConversionSources()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n var listing = Assert.Single(listings, item => item.Id == CodexSwitchDefaults.ManagedCodexModel);\n Assert.Contains(provider.Id, listing.ProviderIds);\n }\n\n [Fact]\n public void EnsureValidDefaults_SeedsAndPreservesDefaultModelConversions()\n {\n var disabledProvider = new ProviderConfig\n {\n Id = \"disabled\",\n DisplayName = \"Disabled\",\n BaseUrl = \"https://example.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-custom\",\n ModelConversions =\n {\n new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = false\n }\n }\n };\n var config = new AppConfig\n {\n ActiveProviderId = disabledProvider.Id,\n Providers = { disabledProvider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n var provider = Assert.Single(config.Providers, item => item.Id == \"disabled\");\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.False(conversion.Enabled);\n\n conversion.Enabled = true;\n provider.DefaultModel = \"claude-new-default\";\n var selection = ProviderRoutingResolver.Resolve(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n }, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.Equal(\"claude-new-default\", selection?.Model?.UpstreamModel);\n }\n\n [Fact]\n public void IconCacheService_UsesOfficialXiaomiFallbackIconUrl()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n using var httpClient = new HttpClient();\n var icons = new IconCacheService(paths, httpClient);\n\n Assert.Equal(\n \"https://platform.xiaomimimo.com/static/favicon.874c9507.png\",\n icons.GetIconUrl(\"xiaomi\"));\n Assert.Equal(\"xiaomi\", IconCacheService.ResolveModelIconSlug(\"mimo-v2.5-pro\"));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n private static TokenPriceTable FlatTable(decimal price)\n {\n var table = new TokenPriceTable();\n table.Tiers.Add(new PricingTier { UpToTokens = null, PricePerUnit = price });\n return table;\n }\n\n private static void WriteJson<T>(string path, T value)\n {\n Directory.CreateDirectory(Path.GetDirectoryName(path)!);\n File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions));\n }\n\n private static void AssertDefaultConversion(ProviderConfig provider)\n {\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.True(conversion.Enabled);\n Assert.True(conversion.UseDefaultModel);\n Assert.Null(conversion.TargetModel);\n }\n\n private static string CreateTempDirectory()\n {\n var path = Path.Combine(Path.GetTempPath(), \"CodexSwitchTests\", Guid.NewGuid().ToString(\"N\"));\n Directory.CreateDirectory(path);\n return path;\n }\n}\n"}, "dff8483da379298aa19ed74d31d6fc3180731e43": {"t": 1785760554.7237117, "n": 25367, "h": "274cf962f5967676", "c": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CodexSwitch.Models;\nusing CodexSwitch.Proxy;\nusing CodexSwitch.Services;\n\nnamespace CodexSwitch.Tests;\n\npublic sealed class BuiltInCatalogMigrationTests\n{\n private static readonly JsonSerializerOptions JsonOptions = new()\n {\n WriteIndented = true,\n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n Converters = { new JsonStringEnumConverter() }\n };\n\n [Fact]\n public void LoadPricing_UpgradesBuiltInCatalogToOfficialDefaults()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new ModelPricingCatalog\n {\n SchemaVersion = \"1.0\",\n Currency = \"USD\",\n BillingUnitTokens = 1_000_000,\n FastMode =\n {\n DefaultMultiplier = 2m,\n ModelOverrides =\n {\n [\"gpt-5.5\"] = 2.5m\n }\n },\n Models =\n {\n new ModelPricingRule\n {\n Id = \"gpt-5.5\",\n DisplayName = \"GPT-5.5\",\n IconSlug = \"openai\",\n Input = FlatTable(1.25m),\n CachedInput = FlatTable(0.125m),\n Output = FlatTable(10m)\n }\n }\n };\n\n WriteJson(paths.PricingPath, legacy);\n\n var upgraded = store.LoadPricing();\n\n Assert.Equal(BuiltInModelCatalog.PricingSchemaVersion, upgraded.SchemaVersion);\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4-mini\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.3-codex\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-opus-4-7\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-3-5-sonnet\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5\");\n\n var gpt55 = Assert.Single(upgraded.Models, rule => rule.Id == \"gpt-5.5\");\n Assert.Equal(5m, gpt55.Input.Tiers[0].PricePerUnit);\n Assert.Equal(BuiltInModelCatalog.OpenAiLongContextThresholdTokens, gpt55.Input.Tiers[0].UpToTokens);\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5.5*\"));\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5\"));\n\n var deepSeekFlash = Assert.Single(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(\"deepseek-chat\", deepSeekFlash.Aliases);\n Assert.Contains(\"deepseek-reasoner\", deepSeekFlash.Aliases);\n\n var mimoPro = Assert.Single(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"mimo-v2-pro\", mimoPro.Aliases);\n Assert.Equal(BuiltInModelCatalog.XiaomiLongContextThresholdTokens, mimoPro.Input.Tiers[0].UpToTokens);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_ExpandsBuiltInProviderModelLists()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"openai-official\",\n BuiltinId = ProviderTemplateCatalog.OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n Models =\n {\n new ModelRouteConfig { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }\n }\n },\n new ProviderConfig\n {\n Id = \"anthropic\",\n BuiltinId = ProviderTemplateCatalog.AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n Models =\n {\n new ModelRouteConfig { Id = \"claude-sonnet-4-5\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var openAi = Assert.Single(upgraded.Providers, provider => provider.Id == \"openai-official\");\n var anthropic = Assert.Single(upgraded.Providers, provider => provider.Id == \"anthropic\");\n var deepSeek = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n var xiaomi = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.XiaomiBuiltinId, StringComparison.OrdinalIgnoreCase));\n\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4-mini\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.3-codex\");\n AssertDefaultConversion(openAi);\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-opus-4-7\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-3-5-sonnet\");\n AssertDefaultConversion(anthropic);\n Assert.Equal(\"https://api.deepseek.com/v1\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-v4-flash\");\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-reasoner\");\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n AssertDefaultConversion(deepSeek);\n Assert.Equal(ProviderProtocol.OpenAiChat, xiaomi.Protocol);\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2-flash\");\n AssertDefaultConversion(xiaomi);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void DeepSeekTemplate_UsesOpenAiChatEndpointAndRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, []);\n\n Assert.Equal(\"https://api.deepseek.com/v1\", provider.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, provider.Protocol);\n Assert.True(provider.SupportsClaudeCode);\n Assert.All(provider.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n }\n\n [Fact]\n public void CodexOAuthTemplate_EnablesWebSocketsByDefault()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.CodexOAuthBuiltinId, []);\n\n Assert.True(provider.SupportsWebSockets == true);\n }\n\n [Fact]\n public void LoadConfig_MigratesCodexOAuthWebSocketDefaultButPreservesExplicitFalse()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var legacy = new AppConfig\n {\n ActiveProviderId = \"codex-oauth\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"codex-oauth\",\n BuiltinId = ProviderTemplateCatalog.CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n BaseUrl = ProviderTemplateCatalog.CodexOAuthTemplate.BaseUrl,\n AuthMode = ProviderAuthMode.OAuth,\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = ProviderTemplateCatalog.CodexOAuthTemplate.DefaultModel\n },\n new ProviderConfig\n {\n Id = \"codex-oauth-disabled\",\n BuiltinId = ProviderTemplateCatalog.CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth Disabled\",\n BaseUrl = ProviderTemplateCatalog.CodexOAuthTemplate.BaseUrl,\n AuthMode = ProviderAuthMode.OAuth,\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = ProviderTemplateCatalog.CodexOAuthTemplate.DefaultModel,\n SupportsWebSockets = false\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n\n Assert.True(upgraded.Providers.Single(provider => provider.Id == \"codex-oauth\").SupportsWebSockets == true);\n Assert.False(upgraded.Providers.Single(provider => provider.Id == \"codex-oauth-disabled\").SupportsWebSockets == true);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_MigratesLegacyDeepSeekBuiltinToOpenAiChat()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"deepseek\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"deepseek\",\n BuiltinId = ProviderTemplateCatalog.DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n BaseUrl = \"https://api.deepseek.com/anthropic\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"deepseek-chat\",\n Models =\n {\n new ModelRouteConfig { Id = \"deepseek-chat\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var deepSeek = Assert.Single(upgraded.Providers, provider => provider.Id == \"deepseek\");\n\n Assert.Equal(ProviderTemplateCatalog.DeepSeekBuiltinId, deepSeek.BuiltinId);\n Assert.Equal(\"https://api.deepseek.com/v1\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Equal(\"deepseek-chat\", deepSeek.DefaultModel);\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void RoutinAiTemplate_IncludesRequestedDeepSeekAndMimoRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n\n var deepSeekFlash = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-flash\");\n var deepSeekPro = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-pro\");\n var mimoFlash = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-flash\");\n var mimoV2Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-pro\");\n var mimoV25Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeekFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeekPro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV2Pro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV25Pro.Protocol);\n Assert.Equal(\"priority\", deepSeekFlash.ServiceTier);\n Assert.Equal(\"priority\", deepSeekPro.ServiceTier);\n Assert.Equal(\"priority\", mimoFlash.ServiceTier);\n Assert.Equal(\"priority\", mimoV2Pro.ServiceTier);\n Assert.Equal(\"priority\", mimoV25Pro.ServiceTier);\n var deepSeekFlashCost = deepSeekFlash.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Flash cost settings should be seeded.\");\n var deepSeekProCost = deepSeekPro.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Pro cost settings should be seeded.\");\n var mimoFlashCost = mimoFlash.Cost ?? throw new InvalidOperationException(\"MiMo V2 Flash cost settings should be seeded.\");\n var mimoV2ProCost = mimoV2Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2 Pro cost settings should be seeded.\");\n var mimoV25ProCost = mimoV25Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2.5 Pro cost settings should be seeded.\");\n Assert.True(deepSeekFlashCost.FastMode);\n Assert.True(deepSeekProCost.FastMode);\n Assert.True(mimoFlashCost.FastMode);\n Assert.True(mimoV2ProCost.FastMode);\n Assert.True(mimoV25ProCost.FastMode);\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-chat\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-reasoner\");\n }\n\n [Fact]\n public void EnsureValidDefaults_MigratesRoutinAiDeepSeekRoutesToOpenAiChat()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n foreach (var route in provider.Models.Where(route => route.Id.StartsWith(\"deepseek-\", StringComparison.OrdinalIgnoreCase)))\n route.Protocol = ProviderProtocol.OpenAiResponses;\n\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n Assert.Equal(\n ProviderProtocol.OpenAiChat,\n provider.Models.Single(route => route.Id == \"deepseek-v4-flash\").Protocol);\n Assert.Equal(\n ProviderProtocol.OpenAiChat,\n provider.Models.Single(route => route.Id == \"deepseek-v4-pro\").Protocol);\n }\n\n [Fact]\n public void SaveConfig_PreservesEditedBuiltInProviderModelRouteFields()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n var route = provider.Models.Single(model => model.Id == \"gpt-5\");\n route.DisplayName = \"Custom GPT\";\n route.Protocol = ProviderProtocol.OpenAiChat;\n route.UpstreamModel = null;\n route.ServiceTier = null;\n route.Cost = new ProviderCostSettings { FastMode = false };\n\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n store.SaveConfig(config);\n var reloaded = store.LoadConfig();\n var reloadedProvider = Assert.Single(reloaded.Providers, item => item.Id == provider.Id);\n var reloadedRoute = reloadedProvider.Models.Single(model => model.Id == \"gpt-5\");\n\n Assert.Equal(\"Custom GPT\", reloadedRoute.DisplayName);\n Assert.Equal(ProviderProtocol.OpenAiChat, reloadedRoute.Protocol);\n Assert.Null(reloadedRoute.UpstreamModel);\n Assert.Null(reloadedRoute.ServiceTier);\n var cost = Assert.IsType<ProviderCostSettings>(reloadedRoute.Cost);\n Assert.False(cost.FastMode);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_RoutesByRequestedModelAcrossProviders()\n {\n var config = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, [\"openai-official\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, [\"openai-official\", \"anthropic\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, [\"openai-official\", \"anthropic\", \"deepseek\"])\n }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, \"claude-sonnet-4-5\");\n\n Assert.NotNull(selection);\n Assert.Equal(\"anthropic\", selection!.Provider.Id);\n Assert.Equal(\"claude-sonnet-4-5\", selection.Model?.Id);\n\n var deepSeekSelection = ProviderRoutingResolver.Resolve(config, \"deepseek-reasoner\");\n Assert.NotNull(deepSeekSelection);\n Assert.Equal(\"deepseek\", deepSeekSelection!.Provider.Id);\n Assert.Equal(\"deepseek-reasoner\", deepSeekSelection.Model?.Id);\n\n var xiaomiSelection = ProviderRoutingResolver.Resolve(config, \"mimo-v2-pro\");\n Assert.NotNull(xiaomiSelection);\n Assert.Equal(\"xiaomi-mimo\", xiaomiSelection!.Provider.Id);\n Assert.Equal(\"mimo-v2-pro\", xiaomiSelection.Model?.Id);\n\n var listings = ProviderRoutingResolver.CollectModelListings(config);\n var gpt54 = Assert.Single(listings, item => item.Id == \"gpt-5.4\");\n var deepSeekFlash = Assert.Single(listings, item => item.Id == \"deepseek-v4-flash\");\n var mimoPro = Assert.Single(listings, item => item.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"openai-official\", gpt54.ProviderIds);\n Assert.Contains(\"deepseek\", deepSeekFlash.ProviderIds);\n Assert.Contains(\"xiaomi-mimo\", mimoPro.ProviderIds);\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesDefaultConversionForActiveProvider()\n {\n foreach (var templateId in new[]\n {\n ProviderTemplateCatalog.AnthropicBuiltinId,\n ProviderTemplateCatalog.DeepSeekBuiltinId,\n ProviderTemplateCatalog.XiaomiBuiltinId\n })\n {\n var openAi = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []);\n var provider = ProviderTemplateCatalog.CreateProvider(templateId, [openAi.Id]);\n var expectedRoute = provider.Models.FirstOrDefault(model =>\n string.Equals(model.Id, provider.DefaultModel, StringComparison.OrdinalIgnoreCase));\n var expectedUpstream = string.IsNullOrWhiteSpace(expectedRoute?.UpstreamModel)\n ? provider.DefaultModel\n : expectedRoute.UpstreamModel;\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { openAi, provider }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.NotNull(selection);\n Assert.Equal(provider.Id, selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n Assert.Equal(expectedUpstream, selection.Model?.UpstreamModel);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_IgnoresDisabledDefaultConversion()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n conversion.Enabled = false;\n\n Assert.False(ProviderRoutingResolver.ProviderSupports(provider, [CodexSwitchDefaults.ManagedCodexModel]));\n\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n Assert.DoesNotContain(listings, listing =>\n string.Equals(listing.Id, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase));\n }\n\n [Fact]\n public void ProviderRoutingResolver_ListsEnabledConversionSources()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n var listing = Assert.Single(listings, item => item.Id == CodexSwitchDefaults.ManagedCodexModel);\n Assert.Contains(provider.Id, listing.ProviderIds);\n }\n\n [Fact]\n public void EnsureValidDefaults_SeedsAndPreservesDefaultModelConversions()\n {\n var disabledProvider = new ProviderConfig\n {\n Id = \"disabled\",\n DisplayName = \"Disabled\",\n BaseUrl = \"https://example.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-custom\",\n ModelConversions =\n {\n new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = false\n }\n }\n };\n var config = new AppConfig\n {\n ActiveProviderId = disabledProvider.Id,\n Providers = { disabledProvider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n var provider = Assert.Single(config.Providers, item => item.Id == \"disabled\");\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.False(conversion.Enabled);\n\n conversion.Enabled = true;\n provider.DefaultModel = \"claude-new-default\";\n var selection = ProviderRoutingResolver.Resolve(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n }, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.Equal(\"claude-new-default\", selection?.Model?.UpstreamModel);\n }\n\n [Fact]\n public void IconCacheService_UsesOfficialXiaomiFallbackIconUrl()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n using var httpClient = new HttpClient();\n var icons = new IconCacheService(paths, httpClient);\n\n Assert.Equal(\n \"https://platform.xiaomimimo.com/static/favicon.874c9507.png\",\n icons.GetIconUrl(\"xiaomi\"));\n Assert.Equal(\"xiaomi\", IconCacheService.ResolveModelIconSlug(\"mimo-v2.5-pro\"));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n private static TokenPriceTable FlatTable(decimal price)\n {\n var table = new TokenPriceTable();\n table.Tiers.Add(new PricingTier { UpToTokens = null, PricePerUnit = price });\n return table;\n }\n\n private static void WriteJson<T>(string path, T value)\n {\n Directory.CreateDirectory(Path.GetDirectoryName(path)!);\n File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions));\n }\n\n private static void AssertDefaultConversion(ProviderConfig provider)\n {\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.True(conversion.Enabled);\n Assert.True(conversion.UseDefaultModel);\n Assert.Null(conversion.TargetModel);\n }\n\n private static string CreateTempDirectory()\n {\n var path = Path.Combine(Path.GetTempPath(), \"CodexSwitchTests\", Guid.NewGuid().ToString(\"N\"));\n Directory.CreateDirectory(path);\n return path;\n }\n}\n"}, "bf485c63a5863fa7f0a0aa78daadf62a46ebf5f8": {"t": 1785760556.4685566, "n": 22959, "h": "f8552bf30c8f2f37", "c": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CodexSwitch.Models;\nusing CodexSwitch.Proxy;\nusing CodexSwitch.Services;\n\nnamespace CodexSwitch.Tests;\n\npublic sealed class BuiltInCatalogMigrationTests\n{\n private static readonly JsonSerializerOptions JsonOptions = new()\n {\n WriteIndented = true,\n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n Converters = { new JsonStringEnumConverter() }\n };\n\n [Fact]\n public void LoadPricing_UpgradesBuiltInCatalogToOfficialDefaults()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new ModelPricingCatalog\n {\n SchemaVersion = \"1.0\",\n Currency = \"USD\",\n BillingUnitTokens = 1_000_000,\n FastMode =\n {\n DefaultMultiplier = 2m,\n ModelOverrides =\n {\n [\"gpt-5.5\"] = 2.5m\n }\n },\n Models =\n {\n new ModelPricingRule\n {\n Id = \"gpt-5.5\",\n DisplayName = \"GPT-5.5\",\n IconSlug = \"openai\",\n Input = FlatTable(1.25m),\n CachedInput = FlatTable(0.125m),\n Output = FlatTable(10m)\n }\n }\n };\n\n WriteJson(paths.PricingPath, legacy);\n\n var upgraded = store.LoadPricing();\n\n Assert.Equal(BuiltInModelCatalog.PricingSchemaVersion, upgraded.SchemaVersion);\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4-mini\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.3-codex\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-opus-4-7\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-3-5-sonnet\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5\");\n\n var gpt55 = Assert.Single(upgraded.Models, rule => rule.Id == \"gpt-5.5\");\n Assert.Equal(5m, gpt55.Input.Tiers[0].PricePerUnit);\n Assert.Equal(BuiltInModelCatalog.OpenAiLongContextThresholdTokens, gpt55.Input.Tiers[0].UpToTokens);\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5.5*\"));\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5\"));\n\n var deepSeekFlash = Assert.Single(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(\"deepseek-chat\", deepSeekFlash.Aliases);\n Assert.Contains(\"deepseek-reasoner\", deepSeekFlash.Aliases);\n\n var mimoPro = Assert.Single(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"mimo-v2-pro\", mimoPro.Aliases);\n Assert.Equal(BuiltInModelCatalog.XiaomiLongContextThresholdTokens, mimoPro.Input.Tiers[0].UpToTokens);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_ExpandsBuiltInProviderModelLists()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"openai-official\",\n BuiltinId = ProviderTemplateCatalog.OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n Models =\n {\n new ModelRouteConfig { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }\n }\n },\n new ProviderConfig\n {\n Id = \"anthropic\",\n BuiltinId = ProviderTemplateCatalog.AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n Models =\n {\n new ModelRouteConfig { Id = \"claude-sonnet-4-5\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var openAi = Assert.Single(upgraded.Providers, provider => provider.Id == \"openai-official\");\n var anthropic = Assert.Single(upgraded.Providers, provider => provider.Id == \"anthropic\");\n var deepSeek = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n var xiaomi = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.XiaomiBuiltinId, StringComparison.OrdinalIgnoreCase));\n\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4-mini\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.3-codex\");\n AssertDefaultConversion(openAi);\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-opus-4-7\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-3-5-sonnet\");\n AssertDefaultConversion(anthropic);\n Assert.Equal(\"https://api.deepseek.com/v1\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-v4-flash\");\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-reasoner\");\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n AssertDefaultConversion(deepSeek);\n Assert.Equal(ProviderProtocol.OpenAiChat, xiaomi.Protocol);\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2-flash\");\n AssertDefaultConversion(xiaomi);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void DeepSeekTemplate_UsesOpenAiChatEndpointAndRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, []);\n\n Assert.Equal(\"https://api.deepseek.com/v1\", provider.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, provider.Protocol);\n Assert.True(provider.SupportsClaudeCode);\n Assert.All(provider.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n }\n\n [Fact]\n public void LoadConfig_MigratesLegacyDeepSeekBuiltinToOpenAiChat()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"deepseek\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"deepseek\",\n BuiltinId = ProviderTemplateCatalog.DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n BaseUrl = \"https://api.deepseek.com/anthropic\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"deepseek-chat\",\n Models =\n {\n new ModelRouteConfig { Id = \"deepseek-chat\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var deepSeek = Assert.Single(upgraded.Providers, provider => provider.Id == \"deepseek\");\n\n Assert.Equal(ProviderTemplateCatalog.DeepSeekBuiltinId, deepSeek.BuiltinId);\n Assert.Equal(\"https://api.deepseek.com/v1\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Equal(\"deepseek-chat\", deepSeek.DefaultModel);\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.OpenAiChat, model.Protocol));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void RoutinAiTemplate_IncludesRequestedDeepSeekAndMimoRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n\n var deepSeekFlash = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-flash\");\n var deepSeekPro = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-pro\");\n var mimoFlash = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-flash\");\n var mimoV2Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-pro\");\n var mimoV25Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeekFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeekPro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV2Pro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV25Pro.Protocol);\n Assert.Equal(\"priority\", deepSeekFlash.ServiceTier);\n Assert.Equal(\"priority\", deepSeekPro.ServiceTier);\n Assert.Equal(\"priority\", mimoFlash.ServiceTier);\n Assert.Equal(\"priority\", mimoV2Pro.ServiceTier);\n Assert.Equal(\"priority\", mimoV25Pro.ServiceTier);\n var deepSeekFlashCost = deepSeekFlash.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Flash cost settings should be seeded.\");\n var deepSeekProCost = deepSeekPro.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Pro cost settings should be seeded.\");\n var mimoFlashCost = mimoFlash.Cost ?? throw new InvalidOperationException(\"MiMo V2 Flash cost settings should be seeded.\");\n var mimoV2ProCost = mimoV2Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2 Pro cost settings should be seeded.\");\n var mimoV25ProCost = mimoV25Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2.5 Pro cost settings should be seeded.\");\n Assert.True(deepSeekFlashCost.FastMode);\n Assert.True(deepSeekProCost.FastMode);\n Assert.True(mimoFlashCost.FastMode);\n Assert.True(mimoV2ProCost.FastMode);\n Assert.True(mimoV25ProCost.FastMode);\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-chat\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-reasoner\");\n }\n\n [Fact]\n public void EnsureValidDefaults_MigratesRoutinAiDeepSeekRoutesToOpenAiChat()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n foreach (var route in provider.Models.Where(route => route.Id.StartsWith(\"deepseek-\", StringComparison.OrdinalIgnoreCase)))\n route.Protocol = ProviderProtocol.OpenAiResponses;\n\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n Assert.Equal(\n ProviderProtocol.OpenAiChat,\n provider.Models.Single(route => route.Id == \"deepseek-v4-flash\").Protocol);\n Assert.Equal(\n ProviderProtocol.OpenAiChat,\n provider.Models.Single(route => route.Id == \"deepseek-v4-pro\").Protocol);\n }\n\n [Fact]\n public void SaveConfig_PreservesEditedBuiltInProviderModelRouteFields()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n var route = provider.Models.Single(model => model.Id == \"gpt-5\");\n route.DisplayName = \"Custom GPT\";\n route.Protocol = ProviderProtocol.OpenAiChat;\n route.UpstreamModel = null;\n route.ServiceTier = null;\n route.Cost = new ProviderCostSettings { FastMode = false };\n\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n };\n\n store.SaveConfig(config);\n var reloaded = store.LoadConfig();\n var reloadedProvider = Assert.Single(reloaded.Providers, item => item.Id == provider.Id);\n var reloadedRoute = reloadedProvider.Models.Single(model => model.Id == \"gpt-5\");\n\n Assert.Equal(\"Custom GPT\", reloadedRoute.DisplayName);\n Assert.Equal(ProviderProtocol.OpenAiChat, reloadedRoute.Protocol);\n Assert.Null(reloadedRoute.UpstreamModel);\n Assert.Null(reloadedRoute.ServiceTier);\n var cost = Assert.IsType<ProviderCostSettings>(reloadedRoute.Cost);\n Assert.False(cost.FastMode);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_RoutesByRequestedModelAcrossProviders()\n {\n var config = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, [\"openai-official\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, [\"openai-official\", \"anthropic\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, [\"openai-official\", \"anthropic\", \"deepseek\"])\n }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, \"claude-sonnet-4-5\");\n\n Assert.NotNull(selection);\n Assert.Equal(\"anthropic\", selection!.Provider.Id);\n Assert.Equal(\"claude-sonnet-4-5\", selection.Model?.Id);\n\n var deepSeekSelection = ProviderRoutingResolver.Resolve(config, \"deepseek-reasoner\");\n Assert.NotNull(deepSeekSelection);\n Assert.Equal(\"deepseek\", deepSeekSelection!.Provider.Id);\n Assert.Equal(\"deepseek-reasoner\", deepSeekSelection.Model?.Id);\n\n var xiaomiSelection = ProviderRoutingResolver.Resolve(config, \"mimo-v2-pro\");\n Assert.NotNull(xiaomiSelection);\n Assert.Equal(\"xiaomi-mimo\", xiaomiSelection!.Provider.Id);\n Assert.Equal(\"mimo-v2-pro\", xiaomiSelection.Model?.Id);\n\n var listings = ProviderRoutingResolver.CollectModelListings(config);\n var gpt54 = Assert.Single(listings, item => item.Id == \"gpt-5.4\");\n var deepSeekFlash = Assert.Single(listings, item => item.Id == \"deepseek-v4-flash\");\n var mimoPro = Assert.Single(listings, item => item.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"openai-official\", gpt54.ProviderIds);\n Assert.Contains(\"deepseek\", deepSeekFlash.ProviderIds);\n Assert.Contains(\"xiaomi-mimo\", mimoPro.ProviderIds);\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesDefaultConversionForActiveProvider()\n {\n foreach (var templateId in new[]\n {\n ProviderTemplateCatalog.AnthropicBuiltinId,\n ProviderTemplateCatalog.DeepSeekBuiltinId,\n ProviderTemplateCatalog.XiaomiBuiltinId\n })\n {\n var openAi = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []);\n var provider = ProviderTemplateCatalog.CreateProvider(templateId, [openAi.Id]);\n var expectedRoute = provider.Models.FirstOrDefault(model =>\n string.Equals(model.Id, provider.DefaultModel, StringComparison.OrdinalIgnoreCase));\n var expectedUpstream = string.IsNullOrWhiteSpace(expectedRoute?.UpstreamModel)\n ? provider.DefaultModel\n : expectedRoute.UpstreamModel;\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { openAi, provider }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.NotNull(selection);\n Assert.Equal(provider.Id, selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n Assert.Equal(expectedUpstream, selection.Model?.UpstreamModel);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_IgnoresDisabledDefaultConversion()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n conversion.Enabled = false;\n\n Assert.False(ProviderRoutingResolver.ProviderSupports(provider, [CodexSwitchDefaults.ManagedCodexModel]));\n\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n Assert.DoesNotContain(listings, listing =>\n string.Equals(listing.Id, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase));\n }\n\n [Fact]\n public void ProviderRoutingResolver_ListsEnabledConversionSources()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n var listing = Assert.Single(listings, item => item.Id == CodexSwitchDefaults.ManagedCodexModel);\n Assert.Contains(provider.Id, listing.ProviderIds);\n }\n\n [Fact]\n public void EnsureValidDefaults_SeedsAndPreservesDefaultModelConversions()\n {\n var disabledProvider = new ProviderConfig\n {\n Id = \"disabled\",\n DisplayName = \"Disabled\",\n BaseUrl = \"https://example.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-custom\",\n ModelConversions =\n {\n new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = false\n }\n }\n };\n var config = new AppConfig\n {\n ActiveProviderId = disabledProvider.Id,\n Providers = { disabledProvider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n var provider = Assert.Single(config.Providers, item => item.Id == \"disabled\");\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.False(conversion.Enabled);\n\n conversion.Enabled = true;\n provider.DefaultModel = \"claude-new-default\";\n var selection = ProviderRoutingResolver.Resolve(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n }, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.Equal(\"claude-new-default\", selection?.Model?.UpstreamModel);\n }\n\n [Fact]\n public void IconCacheService_UsesOfficialXiaomiFallbackIconUrl()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n using var httpClient = new HttpClient();\n var icons = new IconCacheService(paths, httpClient);\n\n Assert.Equal(\n \"https://platform.xiaomimimo.com/static/favicon.874c9507.png\",\n icons.GetIconUrl(\"xiaomi\"));\n Assert.Equal(\"xiaomi\", IconCacheService.ResolveModelIconSlug(\"mimo-v2.5-pro\"));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n private static TokenPriceTable FlatTable(decimal price)\n {\n var table = new TokenPriceTable();\n table.Tiers.Add(new PricingTier { UpToTokens = null, PricePerUnit = price });\n return table;\n }\n\n private static void WriteJson<T>(string path, T value)\n {\n Directory.CreateDirectory(Path.GetDirectoryName(path)!);\n File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions));\n }\n\n private static void AssertDefaultConversion(ProviderConfig provider)\n {\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.True(conversion.Enabled);\n Assert.True(conversion.UseDefaultModel);\n Assert.Null(conversion.TargetModel);\n }\n\n private static string CreateTempDirectory()\n {\n var path = Path.Combine(Path.GetTempPath(), \"CodexSwitchTests\", Guid.NewGuid().ToString(\"N\"));\n Directory.CreateDirectory(path);\n return path;\n }\n}\n"}, "9f065d119f2b8b24784f9af33eb5e564d6c57bd4": {"t": 1785760558.1934247, "n": 20467, "h": "b963546abeccb597", "c": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CodexSwitch.Models;\nusing CodexSwitch.Proxy;\nusing CodexSwitch.Services;\n\nnamespace CodexSwitch.Tests;\n\npublic sealed class BuiltInCatalogMigrationTests\n{\n private static readonly JsonSerializerOptions JsonOptions = new()\n {\n WriteIndented = true,\n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n Converters = { new JsonStringEnumConverter() }\n };\n\n [Fact]\n public void LoadPricing_UpgradesBuiltInCatalogToOfficialDefaults()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new ModelPricingCatalog\n {\n SchemaVersion = \"1.0\",\n Currency = \"USD\",\n BillingUnitTokens = 1_000_000,\n FastMode =\n {\n DefaultMultiplier = 2m,\n ModelOverrides =\n {\n [\"gpt-5.5\"] = 2.5m\n }\n },\n Models =\n {\n new ModelPricingRule\n {\n Id = \"gpt-5.5\",\n DisplayName = \"GPT-5.5\",\n IconSlug = \"openai\",\n Input = FlatTable(1.25m),\n CachedInput = FlatTable(0.125m),\n Output = FlatTable(10m)\n }\n }\n };\n\n WriteJson(paths.PricingPath, legacy);\n\n var upgraded = store.LoadPricing();\n\n Assert.Equal(BuiltInModelCatalog.PricingSchemaVersion, upgraded.SchemaVersion);\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4-mini\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.3-codex\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-opus-4-7\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-3-5-sonnet\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5\");\n\n var gpt55 = Assert.Single(upgraded.Models, rule => rule.Id == \"gpt-5.5\");\n Assert.Equal(5m, gpt55.Input.Tiers[0].PricePerUnit);\n Assert.Equal(BuiltInModelCatalog.OpenAiLongContextThresholdTokens, gpt55.Input.Tiers[0].UpToTokens);\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5.5*\"));\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5\"));\n\n var deepSeekFlash = Assert.Single(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(\"deepseek-chat\", deepSeekFlash.Aliases);\n Assert.Contains(\"deepseek-reasoner\", deepSeekFlash.Aliases);\n\n var mimoPro = Assert.Single(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"mimo-v2-pro\", mimoPro.Aliases);\n Assert.Equal(BuiltInModelCatalog.XiaomiLongContextThresholdTokens, mimoPro.Input.Tiers[0].UpToTokens);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_ExpandsBuiltInProviderModelLists()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"openai-official\",\n BuiltinId = ProviderTemplateCatalog.OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n Models =\n {\n new ModelRouteConfig { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }\n }\n },\n new ProviderConfig\n {\n Id = \"anthropic\",\n BuiltinId = ProviderTemplateCatalog.AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n Models =\n {\n new ModelRouteConfig { Id = \"claude-sonnet-4-5\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var openAi = Assert.Single(upgraded.Providers, provider => provider.Id == \"openai-official\");\n var anthropic = Assert.Single(upgraded.Providers, provider => provider.Id == \"anthropic\");\n var deepSeek = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n var xiaomi = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.XiaomiBuiltinId, StringComparison.OrdinalIgnoreCase));\n\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4-mini\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.3-codex\");\n AssertDefaultConversion(openAi);\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-opus-4-7\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-3-5-sonnet\");\n AssertDefaultConversion(anthropic);\n Assert.Equal(\"https://api.deepseek.com/anthropic\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.AnthropicMessages, deepSeek.Protocol);\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-v4-flash\");\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-reasoner\");\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.AnthropicMessages, model.Protocol));\n AssertDefaultConversion(deepSeek);\n Assert.Equal(ProviderProtocol.OpenAiChat, xiaomi.Protocol);\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2-flash\");\n AssertDefaultConversion(xiaomi);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void DeepSeekTemplate_UsesAnthropicMessagesEndpointAndRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, []);\n\n Assert.Equal(\"https://api.deepseek.com/anthropic\", provider.BaseUrl);\n Assert.Equal(ProviderProtocol.AnthropicMessages, provider.Protocol);\n Assert.True(provider.SupportsClaudeCode);\n Assert.All(provider.Models, model => Assert.Equal(ProviderProtocol.AnthropicMessages, model.Protocol));\n }\n\n [Fact]\n public void LoadConfig_MigratesLegacyDeepSeekBuiltinToAnthropicMessages()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"deepseek\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"deepseek\",\n BuiltinId = ProviderTemplateCatalog.DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n BaseUrl = \"https://api.deepseek.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"deepseek-chat\",\n Models =\n {\n new ModelRouteConfig { Id = \"deepseek-chat\", Protocol = ProviderProtocol.OpenAiChat }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var deepSeek = Assert.Single(upgraded.Providers, provider => provider.Id == \"deepseek\");\n\n Assert.Equal(ProviderTemplateCatalog.DeepSeekBuiltinId, deepSeek.BuiltinId);\n Assert.Equal(\"https://api.deepseek.com/anthropic\", deepSeek.BaseUrl);\n Assert.Equal(ProviderProtocol.AnthropicMessages, deepSeek.Protocol);\n Assert.Equal(\"deepseek-chat\", deepSeek.DefaultModel);\n Assert.All(deepSeek.Models, model => Assert.Equal(ProviderProtocol.AnthropicMessages, model.Protocol));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void RoutinAiTemplate_IncludesRequestedDeepSeekAndMimoRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n\n var deepSeekFlash = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-flash\");\n var deepSeekPro = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-pro\");\n var mimoFlash = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-flash\");\n var mimoV2Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-pro\");\n var mimoV25Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n\n Assert.Equal(ProviderProtocol.OpenAiResponses, deepSeekFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, deepSeekPro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV2Pro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV25Pro.Protocol);\n Assert.Equal(\"priority\", deepSeekFlash.ServiceTier);\n Assert.Equal(\"priority\", deepSeekPro.ServiceTier);\n Assert.Equal(\"priority\", mimoFlash.ServiceTier);\n Assert.Equal(\"priority\", mimoV2Pro.ServiceTier);\n Assert.Equal(\"priority\", mimoV25Pro.ServiceTier);\n var deepSeekFlashCost = deepSeekFlash.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Flash cost settings should be seeded.\");\n var deepSeekProCost = deepSeekPro.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Pro cost settings should be seeded.\");\n var mimoFlashCost = mimoFlash.Cost ?? throw new InvalidOperationException(\"MiMo V2 Flash cost settings should be seeded.\");\n var mimoV2ProCost = mimoV2Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2 Pro cost settings should be seeded.\");\n var mimoV25ProCost = mimoV25Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2.5 Pro cost settings should be seeded.\");\n Assert.True(deepSeekFlashCost.FastMode);\n Assert.True(deepSeekProCost.FastMode);\n Assert.True(mimoFlashCost.FastMode);\n Assert.True(mimoV2ProCost.FastMode);\n Assert.True(mimoV25ProCost.FastMode);\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-chat\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-reasoner\");\n }\n\n [Fact]\n public void ProviderRoutingResolver_RoutesByRequestedModelAcrossProviders()\n {\n var config = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, [\"openai-official\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, [\"openai-official\", \"anthropic\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, [\"openai-official\", \"anthropic\", \"deepseek\"])\n }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, \"claude-sonnet-4-5\");\n\n Assert.NotNull(selection);\n Assert.Equal(\"anthropic\", selection!.Provider.Id);\n Assert.Equal(\"claude-sonnet-4-5\", selection.Model?.Id);\n\n var deepSeekSelection = ProviderRoutingResolver.Resolve(config, \"deepseek-reasoner\");\n Assert.NotNull(deepSeekSelection);\n Assert.Equal(\"deepseek\", deepSeekSelection!.Provider.Id);\n Assert.Equal(\"deepseek-reasoner\", deepSeekSelection.Model?.Id);\n\n var xiaomiSelection = ProviderRoutingResolver.Resolve(config, \"mimo-v2-pro\");\n Assert.NotNull(xiaomiSelection);\n Assert.Equal(\"xiaomi-mimo\", xiaomiSelection!.Provider.Id);\n Assert.Equal(\"mimo-v2-pro\", xiaomiSelection.Model?.Id);\n\n var listings = ProviderRoutingResolver.CollectModelListings(config);\n var gpt54 = Assert.Single(listings, item => item.Id == \"gpt-5.4\");\n var deepSeekFlash = Assert.Single(listings, item => item.Id == \"deepseek-v4-flash\");\n var mimoPro = Assert.Single(listings, item => item.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"openai-official\", gpt54.ProviderIds);\n Assert.Contains(\"deepseek\", deepSeekFlash.ProviderIds);\n Assert.Contains(\"xiaomi-mimo\", mimoPro.ProviderIds);\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesDefaultConversionForActiveProvider()\n {\n foreach (var templateId in new[]\n {\n ProviderTemplateCatalog.AnthropicBuiltinId,\n ProviderTemplateCatalog.DeepSeekBuiltinId,\n ProviderTemplateCatalog.XiaomiBuiltinId\n })\n {\n var openAi = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []);\n var provider = ProviderTemplateCatalog.CreateProvider(templateId, [openAi.Id]);\n var expectedRoute = provider.Models.FirstOrDefault(model =>\n string.Equals(model.Id, provider.DefaultModel, StringComparison.OrdinalIgnoreCase));\n var expectedUpstream = string.IsNullOrWhiteSpace(expectedRoute?.UpstreamModel)\n ? provider.DefaultModel\n : expectedRoute.UpstreamModel;\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { openAi, provider }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.NotNull(selection);\n Assert.Equal(provider.Id, selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n Assert.Equal(expectedUpstream, selection.Model?.UpstreamModel);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_IgnoresDisabledDefaultConversion()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n conversion.Enabled = false;\n\n Assert.False(ProviderRoutingResolver.ProviderSupports(provider, [CodexSwitchDefaults.ManagedCodexModel]));\n\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n Assert.DoesNotContain(listings, listing =>\n string.Equals(listing.Id, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase));\n }\n\n [Fact]\n public void ProviderRoutingResolver_ListsEnabledConversionSources()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n var listing = Assert.Single(listings, item => item.Id == CodexSwitchDefaults.ManagedCodexModel);\n Assert.Contains(provider.Id, listing.ProviderIds);\n }\n\n [Fact]\n public void EnsureValidDefaults_SeedsAndPreservesDefaultModelConversions()\n {\n var disabledProvider = new ProviderConfig\n {\n Id = \"disabled\",\n DisplayName = \"Disabled\",\n BaseUrl = \"https://example.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-custom\",\n ModelConversions =\n {\n new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = false\n }\n }\n };\n var config = new AppConfig\n {\n ActiveProviderId = disabledProvider.Id,\n Providers = { disabledProvider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n var provider = Assert.Single(config.Providers, item => item.Id == \"disabled\");\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.False(conversion.Enabled);\n\n conversion.Enabled = true;\n provider.DefaultModel = \"claude-new-default\";\n var selection = ProviderRoutingResolver.Resolve(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n }, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.Equal(\"claude-new-default\", selection?.Model?.UpstreamModel);\n }\n\n [Fact]\n public void IconCacheService_UsesOfficialXiaomiFallbackIconUrl()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n using var httpClient = new HttpClient();\n var icons = new IconCacheService(paths, httpClient);\n\n Assert.Equal(\n \"https://platform.xiaomimimo.com/static/favicon.874c9507.png\",\n icons.GetIconUrl(\"xiaomi\"));\n Assert.Equal(\"xiaomi\", IconCacheService.ResolveModelIconSlug(\"mimo-v2.5-pro\"));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n private static TokenPriceTable FlatTable(decimal price)\n {\n var table = new TokenPriceTable();\n table.Tiers.Add(new PricingTier { UpToTokens = null, PricePerUnit = price });\n return table;\n }\n\n private static void WriteJson<T>(string path, T value)\n {\n Directory.CreateDirectory(Path.GetDirectoryName(path)!);\n File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions));\n }\n\n private static void AssertDefaultConversion(ProviderConfig provider)\n {\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.True(conversion.Enabled);\n Assert.True(conversion.UseDefaultModel);\n Assert.Null(conversion.TargetModel);\n }\n\n private static string CreateTempDirectory()\n {\n var path = Path.Combine(Path.GetTempPath(), \"CodexSwitchTests\", Guid.NewGuid().ToString(\"N\"));\n Directory.CreateDirectory(path);\n return path;\n }\n}\n"}, "990006980310ecc5120fdd0738e6265ec5bdb558": {"t": 1785760560.0554986, "n": 17898, "h": "f6ee2c425fd80ff3", "c": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CodexSwitch.Models;\nusing CodexSwitch.Proxy;\nusing CodexSwitch.Services;\n\nnamespace CodexSwitch.Tests;\n\npublic sealed class BuiltInCatalogMigrationTests\n{\n private static readonly JsonSerializerOptions JsonOptions = new()\n {\n WriteIndented = true,\n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n Converters = { new JsonStringEnumConverter() }\n };\n\n [Fact]\n public void LoadPricing_UpgradesBuiltInCatalogToOfficialDefaults()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new ModelPricingCatalog\n {\n SchemaVersion = \"1.0\",\n Currency = \"USD\",\n BillingUnitTokens = 1_000_000,\n FastMode =\n {\n DefaultMultiplier = 2m,\n ModelOverrides =\n {\n [\"gpt-5.5\"] = 2.5m\n }\n },\n Models =\n {\n new ModelPricingRule\n {\n Id = \"gpt-5.5\",\n DisplayName = \"GPT-5.5\",\n IconSlug = \"openai\",\n Input = FlatTable(1.25m),\n CachedInput = FlatTable(0.125m),\n Output = FlatTable(10m)\n }\n }\n };\n\n WriteJson(paths.PricingPath, legacy);\n\n var upgraded = store.LoadPricing();\n\n Assert.Equal(BuiltInModelCatalog.PricingSchemaVersion, upgraded.SchemaVersion);\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4-mini\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.3-codex\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-opus-4-7\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-3-5-sonnet\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5\");\n\n var gpt55 = Assert.Single(upgraded.Models, rule => rule.Id == \"gpt-5.5\");\n Assert.Equal(5m, gpt55.Input.Tiers[0].PricePerUnit);\n Assert.Equal(BuiltInModelCatalog.OpenAiLongContextThresholdTokens, gpt55.Input.Tiers[0].UpToTokens);\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5.5*\"));\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5\"));\n\n var deepSeekFlash = Assert.Single(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(\"deepseek-chat\", deepSeekFlash.Aliases);\n Assert.Contains(\"deepseek-reasoner\", deepSeekFlash.Aliases);\n\n var mimoPro = Assert.Single(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"mimo-v2-pro\", mimoPro.Aliases);\n Assert.Equal(BuiltInModelCatalog.XiaomiLongContextThresholdTokens, mimoPro.Input.Tiers[0].UpToTokens);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_ExpandsBuiltInProviderModelLists()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"openai-official\",\n BuiltinId = ProviderTemplateCatalog.OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n Models =\n {\n new ModelRouteConfig { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }\n }\n },\n new ProviderConfig\n {\n Id = \"anthropic\",\n BuiltinId = ProviderTemplateCatalog.AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n Models =\n {\n new ModelRouteConfig { Id = \"claude-sonnet-4-5\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var openAi = Assert.Single(upgraded.Providers, provider => provider.Id == \"openai-official\");\n var anthropic = Assert.Single(upgraded.Providers, provider => provider.Id == \"anthropic\");\n var deepSeek = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n var xiaomi = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.XiaomiBuiltinId, StringComparison.OrdinalIgnoreCase));\n\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4-mini\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.3-codex\");\n AssertDefaultConversion(openAi);\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-opus-4-7\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-3-5-sonnet\");\n AssertDefaultConversion(anthropic);\n Assert.Equal(ProviderProtocol.AnthropicMessages, deepSeek.Protocol);\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-v4-flash\");\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-reasoner\");\n AssertDefaultConversion(deepSeek);\n Assert.Equal(ProviderProtocol.OpenAiChat, xiaomi.Protocol);\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2-flash\");\n AssertDefaultConversion(xiaomi);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void RoutinAiTemplate_IncludesRequestedDeepSeekAndMimoRoutes()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.RoutinAiBuiltinId, []);\n\n var deepSeekFlash = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-flash\");\n var deepSeekPro = Assert.Single(provider.Models, model => model.Id == \"deepseek-v4-pro\");\n var mimoFlash = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-flash\");\n var mimoV2Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2-pro\");\n var mimoV25Pro = Assert.Single(provider.Models, model => model.Id == \"mimo-v2.5-pro\");\n\n Assert.Equal(ProviderProtocol.OpenAiResponses, deepSeekFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, deepSeekPro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoFlash.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV2Pro.Protocol);\n Assert.Equal(ProviderProtocol.OpenAiResponses, mimoV25Pro.Protocol);\n Assert.Equal(\"priority\", deepSeekFlash.ServiceTier);\n Assert.Equal(\"priority\", deepSeekPro.ServiceTier);\n Assert.Equal(\"priority\", mimoFlash.ServiceTier);\n Assert.Equal(\"priority\", mimoV2Pro.ServiceTier);\n Assert.Equal(\"priority\", mimoV25Pro.ServiceTier);\n var deepSeekFlashCost = deepSeekFlash.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Flash cost settings should be seeded.\");\n var deepSeekProCost = deepSeekPro.Cost ?? throw new InvalidOperationException(\"DeepSeek V4 Pro cost settings should be seeded.\");\n var mimoFlashCost = mimoFlash.Cost ?? throw new InvalidOperationException(\"MiMo V2 Flash cost settings should be seeded.\");\n var mimoV2ProCost = mimoV2Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2 Pro cost settings should be seeded.\");\n var mimoV25ProCost = mimoV25Pro.Cost ?? throw new InvalidOperationException(\"MiMo V2.5 Pro cost settings should be seeded.\");\n Assert.True(deepSeekFlashCost.FastMode);\n Assert.True(deepSeekProCost.FastMode);\n Assert.True(mimoFlashCost.FastMode);\n Assert.True(mimoV2ProCost.FastMode);\n Assert.True(mimoV25ProCost.FastMode);\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-chat\");\n Assert.DoesNotContain(provider.Models, model => model.Id == \"deepseek-reasoner\");\n }\n\n [Fact]\n public void ProviderRoutingResolver_RoutesByRequestedModelAcrossProviders()\n {\n var config = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, [\"openai-official\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, [\"openai-official\", \"anthropic\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, [\"openai-official\", \"anthropic\", \"deepseek\"])\n }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, \"claude-sonnet-4-5\");\n\n Assert.NotNull(selection);\n Assert.Equal(\"anthropic\", selection!.Provider.Id);\n Assert.Equal(\"claude-sonnet-4-5\", selection.Model?.Id);\n\n var deepSeekSelection = ProviderRoutingResolver.Resolve(config, \"deepseek-reasoner\");\n Assert.NotNull(deepSeekSelection);\n Assert.Equal(\"deepseek\", deepSeekSelection!.Provider.Id);\n Assert.Equal(\"deepseek-reasoner\", deepSeekSelection.Model?.Id);\n\n var xiaomiSelection = ProviderRoutingResolver.Resolve(config, \"mimo-v2-pro\");\n Assert.NotNull(xiaomiSelection);\n Assert.Equal(\"xiaomi-mimo\", xiaomiSelection!.Provider.Id);\n Assert.Equal(\"mimo-v2-pro\", xiaomiSelection.Model?.Id);\n\n var listings = ProviderRoutingResolver.CollectModelListings(config);\n var gpt54 = Assert.Single(listings, item => item.Id == \"gpt-5.4\");\n var deepSeekFlash = Assert.Single(listings, item => item.Id == \"deepseek-v4-flash\");\n var mimoPro = Assert.Single(listings, item => item.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"openai-official\", gpt54.ProviderIds);\n Assert.Contains(\"deepseek\", deepSeekFlash.ProviderIds);\n Assert.Contains(\"xiaomi-mimo\", mimoPro.ProviderIds);\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesDefaultConversionForActiveProvider()\n {\n foreach (var templateId in new[]\n {\n ProviderTemplateCatalog.AnthropicBuiltinId,\n ProviderTemplateCatalog.DeepSeekBuiltinId,\n ProviderTemplateCatalog.XiaomiBuiltinId\n })\n {\n var openAi = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []);\n var provider = ProviderTemplateCatalog.CreateProvider(templateId, [openAi.Id]);\n var expectedRoute = provider.Models.FirstOrDefault(model =>\n string.Equals(model.Id, provider.DefaultModel, StringComparison.OrdinalIgnoreCase));\n var expectedUpstream = string.IsNullOrWhiteSpace(expectedRoute?.UpstreamModel)\n ? provider.DefaultModel\n : expectedRoute.UpstreamModel;\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { openAi, provider }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.NotNull(selection);\n Assert.Equal(provider.Id, selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n Assert.Equal(expectedUpstream, selection.Model?.UpstreamModel);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_IgnoresDisabledDefaultConversion()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n conversion.Enabled = false;\n\n Assert.False(ProviderRoutingResolver.ProviderSupports(provider, [CodexSwitchDefaults.ManagedCodexModel]));\n\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n Assert.DoesNotContain(listings, listing =>\n string.Equals(listing.Id, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase));\n }\n\n [Fact]\n public void ProviderRoutingResolver_ListsEnabledConversionSources()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n var listing = Assert.Single(listings, item => item.Id == CodexSwitchDefaults.ManagedCodexModel);\n Assert.Contains(provider.Id, listing.ProviderIds);\n }\n\n [Fact]\n public void EnsureValidDefaults_SeedsAndPreservesDefaultModelConversions()\n {\n var disabledProvider = new ProviderConfig\n {\n Id = \"disabled\",\n DisplayName = \"Disabled\",\n BaseUrl = \"https://example.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-custom\",\n ModelConversions =\n {\n new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = false\n }\n }\n };\n var config = new AppConfig\n {\n ActiveProviderId = disabledProvider.Id,\n Providers = { disabledProvider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n var provider = Assert.Single(config.Providers, item => item.Id == \"disabled\");\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.False(conversion.Enabled);\n\n conversion.Enabled = true;\n provider.DefaultModel = \"claude-new-default\";\n var selection = ProviderRoutingResolver.Resolve(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n }, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.Equal(\"claude-new-default\", selection?.Model?.UpstreamModel);\n }\n\n [Fact]\n public void IconCacheService_UsesOfficialXiaomiFallbackIconUrl()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n using var httpClient = new HttpClient();\n var icons = new IconCacheService(paths, httpClient);\n\n Assert.Equal(\n \"https://platform.xiaomimimo.com/static/favicon.874c9507.png\",\n icons.GetIconUrl(\"xiaomi\"));\n Assert.Equal(\"xiaomi\", IconCacheService.ResolveModelIconSlug(\"mimo-v2.5-pro\"));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n private static TokenPriceTable FlatTable(decimal price)\n {\n var table = new TokenPriceTable();\n table.Tiers.Add(new PricingTier { UpToTokens = null, PricePerUnit = price });\n return table;\n }\n\n private static void WriteJson<T>(string path, T value)\n {\n Directory.CreateDirectory(Path.GetDirectoryName(path)!);\n File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions));\n }\n\n private static void AssertDefaultConversion(ProviderConfig provider)\n {\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.True(conversion.Enabled);\n Assert.True(conversion.UseDefaultModel);\n Assert.Null(conversion.TargetModel);\n }\n\n private static string CreateTempDirectory()\n {\n var path = Path.Combine(Path.GetTempPath(), \"CodexSwitchTests\", Guid.NewGuid().ToString(\"N\"));\n Directory.CreateDirectory(path);\n return path;\n }\n}\n"}, "6e3d72419145ed99d97d0cc01b7be97a370f872a": {"t": 1785760561.4481823, "n": 15442, "h": "528beb3d4611652b", "c": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CodexSwitch.Models;\nusing CodexSwitch.Proxy;\nusing CodexSwitch.Services;\n\nnamespace CodexSwitch.Tests;\n\npublic sealed class BuiltInCatalogMigrationTests\n{\n private static readonly JsonSerializerOptions JsonOptions = new()\n {\n WriteIndented = true,\n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n Converters = { new JsonStringEnumConverter() }\n };\n\n [Fact]\n public void LoadPricing_UpgradesBuiltInCatalogToOfficialDefaults()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new ModelPricingCatalog\n {\n SchemaVersion = \"1.0\",\n Currency = \"USD\",\n BillingUnitTokens = 1_000_000,\n FastMode =\n {\n DefaultMultiplier = 2m,\n ModelOverrides =\n {\n [\"gpt-5.5\"] = 2.5m\n }\n },\n Models =\n {\n new ModelPricingRule\n {\n Id = \"gpt-5.5\",\n DisplayName = \"GPT-5.5\",\n IconSlug = \"openai\",\n Input = FlatTable(1.25m),\n CachedInput = FlatTable(0.125m),\n Output = FlatTable(10m)\n }\n }\n };\n\n WriteJson(paths.PricingPath, legacy);\n\n var upgraded = store.LoadPricing();\n\n Assert.Equal(BuiltInModelCatalog.PricingSchemaVersion, upgraded.SchemaVersion);\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4-mini\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.3-codex\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-opus-4-7\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-3-5-sonnet\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5\");\n\n var gpt55 = Assert.Single(upgraded.Models, rule => rule.Id == \"gpt-5.5\");\n Assert.Equal(5m, gpt55.Input.Tiers[0].PricePerUnit);\n Assert.Equal(BuiltInModelCatalog.OpenAiLongContextThresholdTokens, gpt55.Input.Tiers[0].UpToTokens);\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5.5*\"));\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5\"));\n\n var deepSeekFlash = Assert.Single(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(\"deepseek-chat\", deepSeekFlash.Aliases);\n Assert.Contains(\"deepseek-reasoner\", deepSeekFlash.Aliases);\n\n var mimoPro = Assert.Single(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"mimo-v2-pro\", mimoPro.Aliases);\n Assert.Equal(BuiltInModelCatalog.XiaomiLongContextThresholdTokens, mimoPro.Input.Tiers[0].UpToTokens);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_ExpandsBuiltInProviderModelLists()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"openai-official\",\n BuiltinId = ProviderTemplateCatalog.OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n Models =\n {\n new ModelRouteConfig { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }\n }\n },\n new ProviderConfig\n {\n Id = \"anthropic\",\n BuiltinId = ProviderTemplateCatalog.AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n Models =\n {\n new ModelRouteConfig { Id = \"claude-sonnet-4-5\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var openAi = Assert.Single(upgraded.Providers, provider => provider.Id == \"openai-official\");\n var anthropic = Assert.Single(upgraded.Providers, provider => provider.Id == \"anthropic\");\n var deepSeek = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n var xiaomi = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.XiaomiBuiltinId, StringComparison.OrdinalIgnoreCase));\n\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4-mini\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.3-codex\");\n AssertDefaultConversion(openAi);\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-opus-4-7\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-3-5-sonnet\");\n AssertDefaultConversion(anthropic);\n Assert.Equal(ProviderProtocol.AnthropicMessages, deepSeek.Protocol);\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-v4-flash\");\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-reasoner\");\n AssertDefaultConversion(deepSeek);\n Assert.Equal(ProviderProtocol.OpenAiChat, xiaomi.Protocol);\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2-flash\");\n AssertDefaultConversion(xiaomi);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_RoutesByRequestedModelAcrossProviders()\n {\n var config = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, [\"openai-official\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, [\"openai-official\", \"anthropic\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, [\"openai-official\", \"anthropic\", \"deepseek\"])\n }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, \"claude-sonnet-4-5\");\n\n Assert.NotNull(selection);\n Assert.Equal(\"anthropic\", selection!.Provider.Id);\n Assert.Equal(\"claude-sonnet-4-5\", selection.Model?.Id);\n\n var deepSeekSelection = ProviderRoutingResolver.Resolve(config, \"deepseek-reasoner\");\n Assert.NotNull(deepSeekSelection);\n Assert.Equal(\"deepseek\", deepSeekSelection!.Provider.Id);\n Assert.Equal(\"deepseek-reasoner\", deepSeekSelection.Model?.Id);\n\n var xiaomiSelection = ProviderRoutingResolver.Resolve(config, \"mimo-v2-pro\");\n Assert.NotNull(xiaomiSelection);\n Assert.Equal(\"xiaomi-mimo\", xiaomiSelection!.Provider.Id);\n Assert.Equal(\"mimo-v2-pro\", xiaomiSelection.Model?.Id);\n\n var listings = ProviderRoutingResolver.CollectModelListings(config);\n var gpt54 = Assert.Single(listings, item => item.Id == \"gpt-5.4\");\n var deepSeekFlash = Assert.Single(listings, item => item.Id == \"deepseek-v4-flash\");\n var mimoPro = Assert.Single(listings, item => item.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"openai-official\", gpt54.ProviderIds);\n Assert.Contains(\"deepseek\", deepSeekFlash.ProviderIds);\n Assert.Contains(\"xiaomi-mimo\", mimoPro.ProviderIds);\n }\n\n [Fact]\n public void ProviderRoutingResolver_UsesDefaultConversionForActiveProvider()\n {\n foreach (var templateId in new[]\n {\n ProviderTemplateCatalog.AnthropicBuiltinId,\n ProviderTemplateCatalog.DeepSeekBuiltinId,\n ProviderTemplateCatalog.XiaomiBuiltinId\n })\n {\n var openAi = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []);\n var provider = ProviderTemplateCatalog.CreateProvider(templateId, [openAi.Id]);\n var expectedRoute = provider.Models.FirstOrDefault(model =>\n string.Equals(model.Id, provider.DefaultModel, StringComparison.OrdinalIgnoreCase));\n var expectedUpstream = string.IsNullOrWhiteSpace(expectedRoute?.UpstreamModel)\n ? provider.DefaultModel\n : expectedRoute.UpstreamModel;\n var config = new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { openAi, provider }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.NotNull(selection);\n Assert.Equal(provider.Id, selection!.Provider.Id);\n Assert.Equal(CodexSwitchDefaults.ManagedCodexModel, selection.Model?.Id);\n Assert.Equal(expectedUpstream, selection.Model?.UpstreamModel);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_IgnoresDisabledDefaultConversion()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n conversion.Enabled = false;\n\n Assert.False(ProviderRoutingResolver.ProviderSupports(provider, [CodexSwitchDefaults.ManagedCodexModel]));\n\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n Assert.DoesNotContain(listings, listing =>\n string.Equals(listing.Id, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase));\n }\n\n [Fact]\n public void ProviderRoutingResolver_ListsEnabledConversionSources()\n {\n var provider = ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, []);\n var listings = ProviderRoutingResolver.CollectModelListings(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n });\n\n var listing = Assert.Single(listings, item => item.Id == CodexSwitchDefaults.ManagedCodexModel);\n Assert.Contains(provider.Id, listing.ProviderIds);\n }\n\n [Fact]\n public void EnsureValidDefaults_SeedsAndPreservesDefaultModelConversions()\n {\n var disabledProvider = new ProviderConfig\n {\n Id = \"disabled\",\n DisplayName = \"Disabled\",\n BaseUrl = \"https://example.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-custom\",\n ModelConversions =\n {\n new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = false\n }\n }\n };\n var config = new AppConfig\n {\n ActiveProviderId = disabledProvider.Id,\n Providers = { disabledProvider }\n };\n\n ConfigurationStore.EnsureValidDefaults(config);\n\n var provider = Assert.Single(config.Providers, item => item.Id == \"disabled\");\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.False(conversion.Enabled);\n\n conversion.Enabled = true;\n provider.DefaultModel = \"claude-new-default\";\n var selection = ProviderRoutingResolver.Resolve(new AppConfig\n {\n ActiveProviderId = provider.Id,\n Providers = { provider }\n }, CodexSwitchDefaults.ManagedCodexModel);\n\n Assert.Equal(\"claude-new-default\", selection?.Model?.UpstreamModel);\n }\n\n [Fact]\n public void IconCacheService_UsesOfficialXiaomiFallbackIconUrl()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n using var httpClient = new HttpClient();\n var icons = new IconCacheService(paths, httpClient);\n\n Assert.Equal(\n \"https://platform.xiaomimimo.com/static/favicon.874c9507.png\",\n icons.GetIconUrl(\"xiaomi\"));\n Assert.Equal(\"xiaomi\", IconCacheService.ResolveModelIconSlug(\"mimo-v2.5-pro\"));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n private static TokenPriceTable FlatTable(decimal price)\n {\n var table = new TokenPriceTable();\n table.Tiers.Add(new PricingTier { UpToTokens = null, PricePerUnit = price });\n return table;\n }\n\n private static void WriteJson<T>(string path, T value)\n {\n Directory.CreateDirectory(Path.GetDirectoryName(path)!);\n File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions));\n }\n\n private static void AssertDefaultConversion(ProviderConfig provider)\n {\n var conversion = Assert.Single(provider.ModelConversions, ProviderTemplateCatalog.IsDefaultModelConversion);\n Assert.True(conversion.Enabled);\n Assert.True(conversion.UseDefaultModel);\n Assert.Null(conversion.TargetModel);\n }\n\n private static string CreateTempDirectory()\n {\n var path = Path.Combine(Path.GetTempPath(), \"CodexSwitchTests\", Guid.NewGuid().ToString(\"N\"));\n Directory.CreateDirectory(path);\n return path;\n }\n}\n"}, "6a0551a7f12281bb4807b0bb081553ab0bc47d4e": {"t": 1785760562.6905408, "n": 10509, "h": "fcf1fab2a1f70dc4", "c": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CodexSwitch.Models;\nusing CodexSwitch.Proxy;\nusing CodexSwitch.Services;\n\nnamespace CodexSwitch.Tests;\n\npublic sealed class BuiltInCatalogMigrationTests\n{\n private static readonly JsonSerializerOptions JsonOptions = new()\n {\n WriteIndented = true,\n PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n Converters = { new JsonStringEnumConverter() }\n };\n\n [Fact]\n public void LoadPricing_UpgradesBuiltInCatalogToOfficialDefaults()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new ModelPricingCatalog\n {\n SchemaVersion = \"1.0\",\n Currency = \"USD\",\n BillingUnitTokens = 1_000_000,\n FastMode =\n {\n DefaultMultiplier = 2m,\n ModelOverrides =\n {\n [\"gpt-5.5\"] = 2.5m\n }\n },\n Models =\n {\n new ModelPricingRule\n {\n Id = \"gpt-5.5\",\n DisplayName = \"GPT-5.5\",\n IconSlug = \"openai\",\n Input = FlatTable(1.25m),\n CachedInput = FlatTable(0.125m),\n Output = FlatTable(10m)\n }\n }\n };\n\n WriteJson(paths.PricingPath, legacy);\n\n var upgraded = store.LoadPricing();\n\n Assert.Equal(BuiltInModelCatalog.PricingSchemaVersion, upgraded.SchemaVersion);\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.4-mini\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"gpt-5.3-codex\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-opus-4-7\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"claude-3-5-sonnet\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"deepseek-v4-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(upgraded.Models, rule => rule.Id == \"mimo-v2.5\");\n\n var gpt55 = Assert.Single(upgraded.Models, rule => rule.Id == \"gpt-5.5\");\n Assert.Equal(5m, gpt55.Input.Tiers[0].PricePerUnit);\n Assert.Equal(BuiltInModelCatalog.OpenAiLongContextThresholdTokens, gpt55.Input.Tiers[0].UpToTokens);\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5.5*\"));\n Assert.True(upgraded.FastMode.ModelOverrides.ContainsKey(\"gpt-5\"));\n\n var deepSeekFlash = Assert.Single(upgraded.Models, rule => rule.Id == \"deepseek-v4-flash\");\n Assert.Contains(\"deepseek-chat\", deepSeekFlash.Aliases);\n Assert.Contains(\"deepseek-reasoner\", deepSeekFlash.Aliases);\n\n var mimoPro = Assert.Single(upgraded.Models, rule => rule.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"mimo-v2-pro\", mimoPro.Aliases);\n Assert.Equal(BuiltInModelCatalog.XiaomiLongContextThresholdTokens, mimoPro.Input.Tiers[0].UpToTokens);\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void LoadConfig_ExpandsBuiltInProviderModelLists()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n var store = new ConfigurationStore(paths);\n\n var legacy = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n new ProviderConfig\n {\n Id = \"openai-official\",\n BuiltinId = ProviderTemplateCatalog.OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n Models =\n {\n new ModelRouteConfig { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }\n }\n },\n new ProviderConfig\n {\n Id = \"anthropic\",\n BuiltinId = ProviderTemplateCatalog.AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n Models =\n {\n new ModelRouteConfig { Id = \"claude-sonnet-4-5\", Protocol = ProviderProtocol.AnthropicMessages }\n }\n }\n }\n };\n\n WriteJson(paths.ConfigPath, legacy);\n\n var upgraded = store.LoadConfig();\n var openAi = Assert.Single(upgraded.Providers, provider => provider.Id == \"openai-official\");\n var anthropic = Assert.Single(upgraded.Providers, provider => provider.Id == \"anthropic\");\n var deepSeek = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.DeepSeekBuiltinId, StringComparison.OrdinalIgnoreCase));\n var xiaomi = Assert.Single(upgraded.Providers, provider =>\n string.Equals(provider.BuiltinId, ProviderTemplateCatalog.XiaomiBuiltinId, StringComparison.OrdinalIgnoreCase));\n\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.4-mini\");\n Assert.Contains(openAi.Models, model => model.Id == \"gpt-5.3-codex\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-opus-4-7\");\n Assert.Contains(anthropic.Models, model => model.Id == \"claude-3-5-sonnet\");\n Assert.Equal(ProviderProtocol.OpenAiChat, deepSeek.Protocol);\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-v4-flash\");\n Assert.Contains(deepSeek.Models, model => model.Id == \"deepseek-reasoner\");\n Assert.Equal(ProviderProtocol.OpenAiChat, xiaomi.Protocol);\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2.5-pro\");\n Assert.Contains(xiaomi.Models, model => model.Id == \"mimo-v2-flash\");\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n [Fact]\n public void ProviderRoutingResolver_RoutesByRequestedModelAcrossProviders()\n {\n var config = new AppConfig\n {\n ActiveProviderId = \"openai-official\",\n Providers =\n {\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.OpenAiOfficialBuiltinId, []),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.AnthropicBuiltinId, [\"openai-official\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.DeepSeekBuiltinId, [\"openai-official\", \"anthropic\"]),\n ProviderTemplateCatalog.CreateProvider(ProviderTemplateCatalog.XiaomiBuiltinId, [\"openai-official\", \"anthropic\", \"deepseek\"])\n }\n };\n\n var selection = ProviderRoutingResolver.Resolve(config, \"claude-sonnet-4-5\");\n\n Assert.NotNull(selection);\n Assert.Equal(\"anthropic\", selection!.Provider.Id);\n Assert.Equal(\"claude-sonnet-4-5\", selection.Model?.Id);\n\n var deepSeekSelection = ProviderRoutingResolver.Resolve(config, \"deepseek-reasoner\");\n Assert.NotNull(deepSeekSelection);\n Assert.Equal(\"deepseek\", deepSeekSelection!.Provider.Id);\n Assert.Equal(\"deepseek-reasoner\", deepSeekSelection.Model?.Id);\n\n var xiaomiSelection = ProviderRoutingResolver.Resolve(config, \"mimo-v2-pro\");\n Assert.NotNull(xiaomiSelection);\n Assert.Equal(\"xiaomi-mimo\", xiaomiSelection!.Provider.Id);\n Assert.Equal(\"mimo-v2-pro\", xiaomiSelection.Model?.Id);\n\n var listings = ProviderRoutingResolver.CollectModelListings(config);\n var gpt54 = Assert.Single(listings, item => item.Id == \"gpt-5.4\");\n var deepSeekFlash = Assert.Single(listings, item => item.Id == \"deepseek-v4-flash\");\n var mimoPro = Assert.Single(listings, item => item.Id == \"mimo-v2.5-pro\");\n Assert.Contains(\"openai-official\", gpt54.ProviderIds);\n Assert.Contains(\"deepseek\", deepSeekFlash.ProviderIds);\n Assert.Contains(\"xiaomi-mimo\", mimoPro.ProviderIds);\n }\n\n [Fact]\n public void IconCacheService_UsesOfficialXiaomiFallbackIconUrl()\n {\n var root = CreateTempDirectory();\n try\n {\n var paths = new AppPaths(root, Path.Combine(root, \".codex\"));\n using var httpClient = new HttpClient();\n var icons = new IconCacheService(paths, httpClient);\n\n Assert.Equal(\n \"https://platform.xiaomimimo.com/static/favicon.874c9507.png\",\n icons.GetIconUrl(\"xiaomi\"));\n Assert.Equal(\"xiaomi\", IconCacheService.ResolveModelIconSlug(\"mimo-v2.5-pro\"));\n }\n finally\n {\n Directory.Delete(root, recursive: true);\n }\n }\n\n private static TokenPriceTable FlatTable(decimal price)\n {\n var table = new TokenPriceTable();\n table.Tiers.Add(new PricingTier { UpToTokens = null, PricePerUnit = price });\n return table;\n }\n\n private static void WriteJson<T>(string path, T value)\n {\n Directory.CreateDirectory(Path.GetDirectoryName(path)!);\n File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions));\n }\n\n private static string CreateTempDirectory()\n {\n var path = Path.Combine(Path.GetTempPath(), \"CodexSwitchTests\", Guid.NewGuid().ToString(\"N\"));\n Directory.CreateDirectory(path);\n return path;\n }\n}\n"}}, "CodexSwitch/Services/ProviderTemplateCatalog.cs": {"ab93a928c55fc0df679494e185648c1b7e93276d": {"t": 1785746214.9607954, "n": 18762, "h": "1a7559dd14e73234", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n public const string OpenAiOfficialBaseUrl = \"https://api.openai.com/v1\";\n public const string XiaomiLegacyBaseUrl = \"https://api.xiaomimimo.com/v1\";\n private const string CodexClientVersion = \"0.128.0\";\n private const string CodexCliUserAgent = \"codex_cli_rs/\" + CodexClientVersion +\n \" (Windows 10.0.26200; x86_64) vscode/1.110.0\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true,\n SupportsCodex = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = OpenAiOfficialBaseUrl,\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = OpenAiOfficialBaseUrl,\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n SupportsWebSockets = true,\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"localhost\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"User-Agent\"] = CodexCliUserAgent,\n [\"x-codex-beta-features\"] = \"memories\",\n [\"Chatgpt-Account-Id\"] = \"{{chatgptAccountId}}\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n SupportsCodex = template.SupportsCodex,\n SupportsClaudeCode = template.SupportsClaudeCode,\n SupportsWebSockets = template.SupportsWebSockets,\n Codex = new CodexProviderSettings(),\n ClaudeCode = new ClaudeCodeProviderSettings\n {\n Model = template.IsCustom ? \"\" : template.DefaultModel,\n AlwaysThinkingEnabled = true,\n SkipDangerousModePermissionPrompt = true\n },\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.SupportsCodex = seeded.SupportsCodex;\n provider.SupportsClaudeCode = seeded.SupportsClaudeCode;\n provider.SupportsWebSockets = seeded.SupportsWebSockets;\n provider.Codex = CloneCodex(seeded.Codex);\n provider.ClaudeCode = CloneClaudeCode(seeded.ClaudeCode);\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ClaudeCodeProviderSettings CloneClaudeCode(ClaudeCodeProviderSettings source)\n {\n return new ClaudeCodeProviderSettings\n {\n Model = source.Model,\n AlwaysThinkingEnabled = source.AlwaysThinkingEnabled,\n SkipDangerousModePermissionPrompt = source.SkipDangerousModePermissionPrompt,\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static CodexProviderSettings CloneCodex(CodexProviderSettings source)\n {\n return new CodexProviderSettings\n {\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool SupportsCodex { get; init; }\n\n public bool SupportsClaudeCode { get; init; }\n\n public bool SupportsWebSockets { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "c840982149c2ae4e42ba5340408660b51a700005": {"t": 1785760553.6295655, "n": 18762, "h": "1a7559dd14e73234", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n public const string OpenAiOfficialBaseUrl = \"https://api.openai.com/v1\";\n public const string XiaomiLegacyBaseUrl = \"https://api.xiaomimimo.com/v1\";\n private const string CodexClientVersion = \"0.128.0\";\n private const string CodexCliUserAgent = \"codex_cli_rs/\" + CodexClientVersion +\n \" (Windows 10.0.26200; x86_64) vscode/1.110.0\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true,\n SupportsCodex = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = OpenAiOfficialBaseUrl,\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = OpenAiOfficialBaseUrl,\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n SupportsWebSockets = true,\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"localhost\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"User-Agent\"] = CodexCliUserAgent,\n [\"x-codex-beta-features\"] = \"memories\",\n [\"Chatgpt-Account-Id\"] = \"{{chatgptAccountId}}\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n SupportsCodex = template.SupportsCodex,\n SupportsClaudeCode = template.SupportsClaudeCode,\n SupportsWebSockets = template.SupportsWebSockets,\n Codex = new CodexProviderSettings(),\n ClaudeCode = new ClaudeCodeProviderSettings\n {\n Model = template.IsCustom ? \"\" : template.DefaultModel,\n AlwaysThinkingEnabled = true,\n SkipDangerousModePermissionPrompt = true\n },\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.SupportsCodex = seeded.SupportsCodex;\n provider.SupportsClaudeCode = seeded.SupportsClaudeCode;\n provider.SupportsWebSockets = seeded.SupportsWebSockets;\n provider.Codex = CloneCodex(seeded.Codex);\n provider.ClaudeCode = CloneClaudeCode(seeded.ClaudeCode);\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ClaudeCodeProviderSettings CloneClaudeCode(ClaudeCodeProviderSettings source)\n {\n return new ClaudeCodeProviderSettings\n {\n Model = source.Model,\n AlwaysThinkingEnabled = source.AlwaysThinkingEnabled,\n SkipDangerousModePermissionPrompt = source.SkipDangerousModePermissionPrompt,\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static CodexProviderSettings CloneCodex(CodexProviderSettings source)\n {\n return new CodexProviderSettings\n {\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool SupportsCodex { get; init; }\n\n public bool SupportsClaudeCode { get; init; }\n\n public bool SupportsWebSockets { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "dff8483da379298aa19ed74d31d6fc3180731e43": {"t": 1785760555.0707526, "n": 18622, "h": "70acf284ebcecf4d", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n private const string CodexClientVersion = \"0.128.0\";\n private const string CodexCliUserAgent = \"codex_cli_rs/\" + CodexClientVersion +\n \" (Windows 10.0.26200; x86_64) vscode/1.110.0\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true,\n SupportsCodex = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = \"https://api.xiaomimimo.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n SupportsWebSockets = true,\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"localhost\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"User-Agent\"] = CodexCliUserAgent,\n [\"x-codex-beta-features\"] = \"memories\",\n [\"Chatgpt-Account-Id\"] = \"{{chatgptAccountId}}\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n SupportsCodex = template.SupportsCodex,\n SupportsClaudeCode = template.SupportsClaudeCode,\n SupportsWebSockets = template.SupportsWebSockets,\n Codex = new CodexProviderSettings(),\n ClaudeCode = new ClaudeCodeProviderSettings\n {\n Model = template.IsCustom ? \"\" : template.DefaultModel,\n AlwaysThinkingEnabled = true,\n SkipDangerousModePermissionPrompt = true\n },\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.SupportsCodex = seeded.SupportsCodex;\n provider.SupportsClaudeCode = seeded.SupportsClaudeCode;\n provider.SupportsWebSockets = seeded.SupportsWebSockets;\n provider.Codex = CloneCodex(seeded.Codex);\n provider.ClaudeCode = CloneClaudeCode(seeded.ClaudeCode);\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ClaudeCodeProviderSettings CloneClaudeCode(ClaudeCodeProviderSettings source)\n {\n return new ClaudeCodeProviderSettings\n {\n Model = source.Model,\n AlwaysThinkingEnabled = source.AlwaysThinkingEnabled,\n SkipDangerousModePermissionPrompt = source.SkipDangerousModePermissionPrompt,\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static CodexProviderSettings CloneCodex(CodexProviderSettings source)\n {\n return new CodexProviderSettings\n {\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool SupportsCodex { get; init; }\n\n public bool SupportsClaudeCode { get; init; }\n\n public bool SupportsWebSockets { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "377692db842f9bb7e7abbe40cce36a182feff753": {"t": 1785760556.8696904, "n": 18083, "h": "120d2806ca640724", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n private const string CodexClientVersion = \"0.128.0\";\n private const string CodexCliUserAgent = \"codex_cli_rs/\" + CodexClientVersion +\n \" (Windows 10.0.26200; x86_64) vscode/1.110.0\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true,\n SupportsCodex = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = \"https://api.xiaomimimo.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"localhost\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"User-Agent\"] = CodexCliUserAgent,\n [\"x-codex-beta-features\"] = \"memories\",\n [\"Chatgpt-Account-Id\"] = \"{{chatgptAccountId}}\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n SupportsCodex = template.SupportsCodex,\n SupportsClaudeCode = template.SupportsClaudeCode,\n ClaudeCode = new ClaudeCodeProviderSettings\n {\n Model = template.IsCustom ? \"\" : template.DefaultModel,\n AlwaysThinkingEnabled = true,\n SkipDangerousModePermissionPrompt = true\n },\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.SupportsCodex = seeded.SupportsCodex;\n provider.SupportsClaudeCode = seeded.SupportsClaudeCode;\n provider.ClaudeCode = CloneClaudeCode(seeded.ClaudeCode);\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ClaudeCodeProviderSettings CloneClaudeCode(ClaudeCodeProviderSettings source)\n {\n return new ClaudeCodeProviderSettings\n {\n Model = source.Model,\n AlwaysThinkingEnabled = source.AlwaysThinkingEnabled,\n SkipDangerousModePermissionPrompt = source.SkipDangerousModePermissionPrompt,\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool SupportsCodex { get; init; }\n\n public bool SupportsClaudeCode { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "bf485c63a5863fa7f0a0aa78daadf62a46ebf5f8": {"t": 1785760558.5649521, "n": 17713, "h": "3e4b47c4cdd72f87", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true,\n SupportsCodex = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = \"https://api.xiaomimimo.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"127.0.0.1\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n SupportsCodex = template.SupportsCodex,\n SupportsClaudeCode = template.SupportsClaudeCode,\n ClaudeCode = new ClaudeCodeProviderSettings\n {\n Model = template.IsCustom ? \"\" : template.DefaultModel,\n AlwaysThinkingEnabled = true,\n SkipDangerousModePermissionPrompt = true\n },\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.SupportsCodex = seeded.SupportsCodex;\n provider.SupportsClaudeCode = seeded.SupportsClaudeCode;\n provider.ClaudeCode = CloneClaudeCode(seeded.ClaudeCode);\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ClaudeCodeProviderSettings CloneClaudeCode(ClaudeCodeProviderSettings source)\n {\n return new ClaudeCodeProviderSettings\n {\n Model = source.Model,\n AlwaysThinkingEnabled = source.AlwaysThinkingEnabled,\n SkipDangerousModePermissionPrompt = source.SkipDangerousModePermissionPrompt,\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool SupportsCodex { get; init; }\n\n public bool SupportsClaudeCode { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "9f065d119f2b8b24784f9af33eb5e564d6c57bd4": {"t": 1785760560.9324698, "n": 17734, "h": "f662588b2d32bb62", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true,\n SupportsCodex = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek Anthropic-compatible Messages API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/anthropic\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = \"https://api.xiaomimimo.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"127.0.0.1\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n SupportsCodex = template.SupportsCodex,\n SupportsClaudeCode = template.SupportsClaudeCode,\n ClaudeCode = new ClaudeCodeProviderSettings\n {\n Model = template.IsCustom ? \"\" : template.DefaultModel,\n AlwaysThinkingEnabled = true,\n SkipDangerousModePermissionPrompt = true\n },\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.SupportsCodex = seeded.SupportsCodex;\n provider.SupportsClaudeCode = seeded.SupportsClaudeCode;\n provider.ClaudeCode = CloneClaudeCode(seeded.ClaudeCode);\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ClaudeCodeProviderSettings CloneClaudeCode(ClaudeCodeProviderSettings source)\n {\n return new ClaudeCodeProviderSettings\n {\n Model = source.Model,\n AlwaysThinkingEnabled = source.AlwaysThinkingEnabled,\n SkipDangerousModePermissionPrompt = source.SkipDangerousModePermissionPrompt,\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool SupportsCodex { get; init; }\n\n public bool SupportsClaudeCode { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "3124adb486c527ac1a9e995421361cce038d17d7": {"t": 1785760562.276077, "n": 17688, "h": "14cc318db46f32ff", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true,\n SupportsCodex = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n SupportsCodex = true,\n SupportsClaudeCode = true,\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/anthropic\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = \"https://api.xiaomimimo.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n SupportsCodex = true,\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n SupportsCodex = true,\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"127.0.0.1\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n SupportsCodex = template.SupportsCodex,\n SupportsClaudeCode = template.SupportsClaudeCode,\n ClaudeCode = new ClaudeCodeProviderSettings\n {\n Model = template.IsCustom ? \"\" : template.DefaultModel,\n AlwaysThinkingEnabled = true,\n SkipDangerousModePermissionPrompt = true\n },\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.SupportsCodex = seeded.SupportsCodex;\n provider.SupportsClaudeCode = seeded.SupportsClaudeCode;\n provider.ClaudeCode = CloneClaudeCode(seeded.ClaudeCode);\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ClaudeCodeProviderSettings CloneClaudeCode(ClaudeCodeProviderSettings source)\n {\n return new ClaudeCodeProviderSettings\n {\n Model = source.Model,\n AlwaysThinkingEnabled = source.AlwaysThinkingEnabled,\n SkipDangerousModePermissionPrompt = source.SkipDangerousModePermissionPrompt,\n EnableOneMillionContext = source.EnableOneMillionContext\n };\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool SupportsCodex { get; init; }\n\n public bool SupportsClaudeCode { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "990006980310ecc5120fdd0738e6265ec5bdb558": {"t": 1785760563.9570343, "n": 16288, "h": "bd622109736c6454", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = IconCacheService.RoutinAiIconSlug,\n FastMode = true,\n ServiceTier = \"priority\",\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/anthropic\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = \"https://api.xiaomimimo.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"127.0.0.1\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "6e3d72419145ed99d97d0cc01b7be97a370f872a": {"t": 1785760565.2723088, "n": 16238, "h": "cfb06b74729b8c6a", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n FastMode = true,\n ServiceTier = \"priority\",\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n FastMode = true,\n ServiceTier = \"priority\",\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = CodexSwitchDefaults.ManagedCodexModel,\n IconSlug = \"openai\",\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com/anthropic\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = \"https://api.xiaomimimo.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"127.0.0.1\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? CodexSwitchDefaults.ManagedCodexModel : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n UsageQuery = CreateDefaultUsageQuery(template),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = CodexSwitchDefaults.ManagedCodexModel, Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n EnsureDefaultModelConversion(provider);\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.UsageQuery = UsageQueryTemplateCatalog.CloneQuery(seeded.UsageQuery);\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n\n provider.ModelConversions ??= [];\n provider.ModelConversions.Clear();\n foreach (var conversion in seeded.ModelConversions)\n provider.ModelConversions.Add(CloneConversion(conversion));\n }\n\n public static void EnsureDefaultModelConversion(ProviderConfig provider)\n {\n provider.ModelConversions ??= [];\n var existing = provider.ModelConversions.FirstOrDefault(IsDefaultModelConversion);\n if (existing is not null)\n {\n existing.SourceModel = CodexSwitchDefaults.ManagedCodexModel;\n existing.TargetModel = null;\n existing.UseDefaultModel = true;\n return;\n }\n\n provider.ModelConversions.Add(new ModelConversionConfig\n {\n SourceModel = CodexSwitchDefaults.ManagedCodexModel,\n UseDefaultModel = true,\n Enabled = true\n });\n }\n\n public static bool IsDefaultModelConversion(ModelConversionConfig conversion)\n {\n return conversion.UseDefaultModel &&\n string.Equals(conversion.SourceModel, CodexSwitchDefaults.ManagedCodexModel, StringComparison.OrdinalIgnoreCase);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderUsageQueryConfig CreateDefaultUsageQuery(ProviderTemplate template)\n {\n if (string.Equals(template.BuiltinId, RoutinAiBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiApiKeyTemplateId);\n\n if (string.Equals(template.BuiltinId, RoutinAiPlanBuiltinId, StringComparison.OrdinalIgnoreCase))\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.RoutinAiPlanTemplateId);\n\n return UsageQueryTemplateCatalog.CreateQuery(UsageQueryTemplateCatalog.CustomTemplateId);\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static ModelConversionConfig CloneConversion(ModelConversionConfig source)\n {\n return new ModelConversionConfig\n {\n SourceModel = source.SourceModel,\n TargetModel = source.TargetModel,\n UseDefaultModel = source.UseDefaultModel,\n Enabled = source.Enabled\n };\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}, "6a0551a7f12281bb4807b0bb081553ab0bc47d4e": {"t": 1785760566.5084708, "n": 13751, "h": "86c53d794a87f6ba", "c": "namespace CodexSwitch.Services;\n\npublic static class ProviderTemplateCatalog\n{\n public const string CustomTemplateId = \"custom\";\n public const string RoutinAiBuiltinId = \"routinai\";\n public const string RoutinAiPlanBuiltinId = \"routinai-plan\";\n public const string OpenAiOfficialBuiltinId = \"openai-official\";\n public const string AnthropicBuiltinId = \"anthropic\";\n public const string DeepSeekBuiltinId = \"deepseek\";\n public const string XiaomiBuiltinId = \"xiaomi-mimo\";\n public const string CodexOAuthBuiltinId = \"codex-oauth\";\n\n private static readonly ProviderTemplate[] Templates =\n [\n new()\n {\n Id = CustomTemplateId,\n DisplayName = \"Custom Provider\",\n Description = \"Manually configure the provider endpoint and model routes.\",\n IconSlug = \"openai\",\n IsCustom = true\n },\n new()\n {\n Id = RoutinAiBuiltinId,\n BuiltinId = RoutinAiBuiltinId,\n DisplayName = \"RoutinAI\",\n Description = \"RoutinAI OpenAI Responses endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n IconSlug = \"openai\",\n FastMode = true,\n ServiceTier = \"priority\",\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = RoutinAiPlanBuiltinId,\n BuiltinId = RoutinAiPlanBuiltinId,\n DisplayName = \"RoutinAI Plan\",\n Description = \"RoutinAI plan API endpoint\",\n Website = \"https://api.routin.ai\",\n BaseUrl = \"https://api.routin.ai/plan/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n IconSlug = \"openai\",\n FastMode = true,\n ServiceTier = \"priority\",\n Models = BuiltInModelCatalog.RoutinAiModels\n },\n new()\n {\n Id = OpenAiOfficialBuiltinId,\n BuiltinId = OpenAiOfficialBuiltinId,\n DisplayName = \"OpenAI Official\",\n Description = \"Official OpenAI Responses API\",\n Website = \"https://platform.openai.com\",\n BaseUrl = \"https://api.openai.com/v1\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.5\",\n IconSlug = \"openai\",\n Models = BuiltInModelCatalog.OpenAiOfficialModels\n },\n new()\n {\n Id = AnthropicBuiltinId,\n BuiltinId = AnthropicBuiltinId,\n DisplayName = \"Anthropic Messages\",\n Description = \"Official Claude Messages API\",\n Website = \"https://console.anthropic.com\",\n BaseUrl = \"https://api.anthropic.com/v1\",\n Protocol = ProviderProtocol.AnthropicMessages,\n DefaultModel = \"claude-sonnet-4-5\",\n IconSlug = \"claude\",\n Models = BuiltInModelCatalog.AnthropicModels\n },\n new()\n {\n Id = DeepSeekBuiltinId,\n BuiltinId = DeepSeekBuiltinId,\n DisplayName = \"DeepSeek\",\n Description = \"Official DeepSeek OpenAI-compatible chat API\",\n Website = \"https://platform.deepseek.com\",\n BaseUrl = \"https://api.deepseek.com\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"deepseek-v4-flash\",\n IconSlug = \"deepseek\",\n Models = BuiltInModelCatalog.DeepSeekModels\n },\n new()\n {\n Id = XiaomiBuiltinId,\n BuiltinId = XiaomiBuiltinId,\n DisplayName = \"Xiaomi MiMo\",\n Description = \"Official Xiaomi MiMo OpenAI-compatible chat API\",\n Website = \"https://platform.xiaomimimo.com\",\n BaseUrl = \"https://api.xiaomimimo.com/v1\",\n Protocol = ProviderProtocol.OpenAiChat,\n DefaultModel = \"mimo-v2.5-pro\",\n IconSlug = \"xiaomi\",\n Models = BuiltInModelCatalog.XiaomiModels\n }\n ];\n\n public static IReadOnlyList<ProviderTemplate> VisibleTemplates => Templates;\n\n public static ProviderTemplate CustomTemplate => Templates[0];\n\n public static ProviderTemplate CodexOAuthTemplate { get; } = new()\n {\n Id = CodexOAuthBuiltinId,\n BuiltinId = CodexOAuthBuiltinId,\n DisplayName = \"Codex OAuth\",\n Description = \"Sign in with OpenAI Codex OAuth for the ChatGPT Codex backend\",\n Website = \"https://openai.com/codex\",\n BaseUrl = \"https://chatgpt.com/backend-api/codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n DefaultModel = \"gpt-5.1-codex\",\n IconSlug = \"openai\",\n AuthMode = ProviderAuthMode.OAuth,\n FastMode = true,\n ServiceTier = \"priority\",\n OAuth = new ProviderOAuthSettings\n {\n AuthorizeUrl = \"https://auth.openai.com/oauth/authorize\",\n TokenUrl = \"https://auth.openai.com/oauth/token\",\n ClientId = \"app_EMoamEEZ73f0CkXaXp7hrann\",\n ClientIdLocked = true,\n Scope = \"openid profile email offline_access\",\n RefreshScope = \"openid profile email\",\n RedirectHost = \"127.0.0.1\",\n RedirectPort = 1455,\n RedirectPath = \"/auth/callback\",\n UsePkce = true,\n UseJsonRefresh = true\n },\n RequestOverrides = new ProviderRequestOverrides\n {\n ForceStoreFalse = true,\n Instructions = \"\",\n OmitBodyKeys = { \"temperature\", \"max_output_tokens\" },\n Headers =\n {\n [\"openai-beta\"] = \"responses=experimental\",\n [\"originator\"] = \"codex_cli_rs\",\n [\"session_id\"] = \"{{sessionId}}\",\n [\"conversation_id\"] = \"{{sessionId}}\"\n }\n },\n Models =\n [\n new ProviderTemplateModel\n {\n Id = \"gpt-5.1-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n },\n new ProviderTemplateModel\n {\n Id = \"gpt-5-codex\",\n Protocol = ProviderProtocol.OpenAiResponses,\n ServiceTier = \"priority\",\n FastMode = true\n }\n ]\n };\n\n public static ProviderTemplate? Find(string templateId)\n {\n if (string.Equals(templateId, CodexOAuthBuiltinId, StringComparison.OrdinalIgnoreCase))\n return CodexOAuthTemplate;\n\n return Templates.FirstOrDefault(template =>\n string.Equals(template.Id, templateId, StringComparison.OrdinalIgnoreCase));\n }\n\n public static ProviderConfig CreateProvider(string templateId, IEnumerable<string> existingIds)\n {\n var template = Find(templateId) ?? CustomTemplate;\n return CreateProvider(template, existingIds);\n }\n\n public static ProviderConfig CreateProvider(ProviderTemplate template, IEnumerable<string> existingIds)\n {\n var idSeed = string.IsNullOrWhiteSpace(template.BuiltinId) ? template.DisplayName : template.BuiltinId;\n var provider = new ProviderConfig\n {\n Id = MakeUniqueId(CreateProviderId(idSeed), existingIds),\n BuiltinId = template.BuiltinId,\n DisplayName = template.IsCustom ? \"New Provider\" : template.DisplayName,\n Note = template.Description,\n Website = template.Website,\n IconSlug = template.IconSlug,\n BaseUrl = template.IsCustom ? \"https://api.example.com/v1\" : template.BaseUrl,\n ApiKey = \"\",\n AuthMode = template.AuthMode,\n Protocol = template.Protocol,\n DefaultModel = template.IsCustom ? \"gpt-5.5\" : template.DefaultModel,\n OverrideRequestModel = false,\n ServiceTier = template.ServiceTier,\n OAuth = CloneOAuth(template.OAuth),\n RequestOverrides = CloneOverrides(template.RequestOverrides),\n Cost = new ProviderCostSettings { FastMode = template.FastMode }\n };\n\n var models = template.IsCustom\n ? [new ProviderTemplateModel { Id = \"gpt-5.5\", Protocol = ProviderProtocol.OpenAiResponses }]\n : template.Models;\n\n foreach (var model in models)\n {\n provider.Models.Add(new ModelRouteConfig\n {\n Id = model.Id,\n DisplayName = model.DisplayName,\n Protocol = model.Protocol,\n UpstreamModel = model.UpstreamModel,\n ServiceTier = model.ServiceTier,\n Cost = new ProviderCostSettings { FastMode = model.FastMode }\n });\n }\n\n return provider;\n }\n\n public static void ApplyTemplate(ProviderConfig provider, string templateId)\n {\n var template = Find(templateId) ?? CustomTemplate;\n var seeded = CreateProvider(template, [provider.Id]);\n provider.BuiltinId = seeded.BuiltinId;\n provider.DisplayName = seeded.DisplayName;\n provider.Note = seeded.Note;\n provider.Website = seeded.Website;\n provider.IconSlug = seeded.IconSlug;\n provider.BaseUrl = seeded.BaseUrl;\n provider.AuthMode = seeded.AuthMode;\n provider.Protocol = seeded.Protocol;\n provider.DefaultModel = seeded.DefaultModel;\n provider.ServiceTier = seeded.ServiceTier;\n provider.OAuth = seeded.OAuth;\n provider.RequestOverrides = seeded.RequestOverrides;\n provider.Cost = seeded.Cost;\n provider.Models.Clear();\n foreach (var model in seeded.Models)\n provider.Models.Add(model);\n }\n\n public static bool IsChatGptCodexBackend(string? baseUrl)\n {\n if (string.IsNullOrWhiteSpace(baseUrl))\n return false;\n\n if (!Uri.TryCreate(baseUrl.Trim(), UriKind.Absolute, out var uri))\n return false;\n\n return string.Equals(uri.Host, \"chatgpt.com\", StringComparison.OrdinalIgnoreCase) &&\n string.Equals(uri.AbsolutePath.TrimEnd('/'), \"/backend-api/codex\", StringComparison.OrdinalIgnoreCase);\n }\n\n private static ProviderOAuthSettings? CloneOAuth(ProviderOAuthSettings? source)\n {\n if (source is null)\n return null;\n\n return new ProviderOAuthSettings\n {\n AuthorizeUrl = source.AuthorizeUrl,\n TokenUrl = source.TokenUrl,\n ClientId = source.ClientId,\n ClientIdLocked = source.ClientIdLocked,\n Scope = source.Scope,\n RefreshScope = source.RefreshScope,\n RedirectHost = source.RedirectHost,\n RedirectPort = source.RedirectPort,\n RedirectPath = source.RedirectPath,\n UsePkce = source.UsePkce,\n UseJsonRefresh = source.UseJsonRefresh\n };\n }\n\n private static ProviderRequestOverrides? CloneOverrides(ProviderRequestOverrides? source)\n {\n if (source is null)\n return null;\n\n var clone = new ProviderRequestOverrides\n {\n ForceStoreFalse = source.ForceStoreFalse,\n Instructions = source.Instructions\n };\n foreach (var header in source.Headers)\n clone.Headers[header.Key] = header.Value;\n foreach (var key in source.OmitBodyKeys)\n clone.OmitBodyKeys.Add(key);\n\n return clone;\n }\n\n private static string MakeUniqueId(string seed, IEnumerable<string> existingIds)\n {\n var existing = existingIds\n .Where(id => !string.IsNullOrWhiteSpace(id))\n .ToHashSet(StringComparer.OrdinalIgnoreCase);\n\n if (!existing.Contains(seed))\n return seed;\n\n for (var index = 2; ; index++)\n {\n var candidate = $\"{seed}-{index}\";\n if (!existing.Contains(candidate))\n return candidate;\n }\n }\n\n private static string CreateProviderId(string value)\n {\n var chars = value.Trim().ToLowerInvariant()\n .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')\n .ToArray();\n var id = new string(chars).Trim('-');\n while (id.Contains(\"--\", StringComparison.Ordinal))\n id = id.Replace(\"--\", \"-\", StringComparison.Ordinal);\n return string.IsNullOrWhiteSpace(id) ? \"provider\" : id;\n }\n}\n\npublic sealed class ProviderTemplate\n{\n public string Id { get; init; } = \"\";\n\n public string? BuiltinId { get; init; }\n\n public string DisplayName { get; init; } = \"\";\n\n public string Description { get; init; } = \"\";\n\n public string? Website { get; init; }\n\n public string IconSlug { get; init; } = \"openai\";\n\n public string BaseUrl { get; init; } = \"\";\n\n public ProviderAuthMode AuthMode { get; init; } = ProviderAuthMode.ApiKey;\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string DefaultModel { get; init; } = \"\";\n\n public bool FastMode { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool IsCustom { get; init; }\n\n public ProviderOAuthSettings? OAuth { get; init; }\n\n public ProviderRequestOverrides? RequestOverrides { get; init; }\n\n public IReadOnlyList<ProviderTemplateModel> Models { get; init; } = [];\n}\n\npublic sealed class ProviderTemplateModel\n{\n public string Id { get; init; } = \"\";\n\n public string? DisplayName { get; init; }\n\n public ProviderProtocol Protocol { get; init; } = ProviderProtocol.OpenAiResponses;\n\n public string? UpstreamModel { get; init; }\n\n public string? ServiceTier { get; init; }\n\n public bool FastMode { get; init; }\n}\n"}}}}