@opengeni/config 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/config",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "OpenGeni runtime configuration: settings resolution, deployment knobs, and config validation shared across the server packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -33,8 +33,8 @@
33
33
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
34
34
  },
35
35
  "dependencies": {
36
- "@opengeni/codex": "^0.2.24",
37
- "@opengeni/contracts": "^5.0.0",
36
+ "@opengeni/codex": "^0.2.25",
37
+ "@opengeni/contracts": "^5.1.0",
38
38
  "@opengeni/xai-subscription": "^0.1.4",
39
39
  "zod": "^4.2.1"
40
40
  },
package/src/index.ts CHANGED
@@ -395,6 +395,7 @@ const SettingsSchema = z.object({
395
395
  environmentsEncryptionKey: z.string().optional(),
396
396
  integrationsEnabled: EnvBoolean.default(false),
397
397
  integrationsStateSecret: z.string().optional(),
398
+ integrationsOauthShortStateEnabled: EnvBoolean.default(false),
398
399
  integrationsAllowPrivateNetworkTargets: EnvBoolean.default(false),
399
400
  integrationsOauthClientsJson: z.string().default("{}"),
400
401
  slackClientId: z.string().optional(),
@@ -556,8 +557,8 @@ const SettingsSchema = z.object({
556
557
  openaiProvider: z.enum(["openai", "azure"]).default("openai"),
557
558
  openaiApiKey: z.string().optional(),
558
559
  openaiBaseUrl: z.string().optional(),
559
- openaiModel: z.string().default("gpt-5.6-sol"),
560
- openaiAllowedModels: z.string().default("gpt-5.6-sol,gpt-5.6-terra,gpt-5.6-luna"),
560
+ openaiModel: z.string().default("gpt-6-astra"),
561
+ openaiAllowedModels: z.string().default("gpt-6-astra,gpt-6-sol,gpt-6-luna"),
561
562
  // OpenGeni-managed Vercel AI Gateway. When configured, the two reviewed
562
563
  // Gateway models below are added to the managed-credit catalog. Workspace
563
564
  // Gateway keys use the encrypted connection broker and never this secret.
@@ -686,6 +687,7 @@ const SettingsSchema = z.object({
686
687
  // binding so database mode cannot be bypassed with a second source.
687
688
  resolvedGatewayModelsJson: z.string().optional(),
688
689
  resolvedOpenRouterModelsJson: z.string().optional(),
690
+ resolvedCodexModelsJson: z.string().optional(),
689
691
  // Extra (non-built-in) model providers, declared by the host as a JSON
690
692
  // provider registry. Each entry carries its own base URL, API key, wire API
691
693
  // ("responses" | "chat") and the models it exposes. The models a client may
@@ -1553,6 +1555,10 @@ export type VoiceInputProviderConfig =
1553
1555
  experimental: true;
1554
1556
  };
1555
1557
 
1558
+ function usableDeploymentSecret(value: string | null | undefined): string | undefined {
1559
+ return isUsableVoiceInputSecret(value) ? value : undefined;
1560
+ }
1561
+
1556
1562
  /**
1557
1563
  * Reject empty / template secrets so `.env.example` placeholders like
1558
1564
  * `your-key` cannot advertise voice input as available and then 401 upstream.
@@ -2244,6 +2250,24 @@ const DeploymentGatewayCatalogModelSchema = GatewayCatalogModel.safeExtend({
2244
2250
  pricing: z.never().optional(),
2245
2251
  }).strict();
2246
2252
 
2253
+ const CodexCatalogModelSchema = RegistryModelSchema.safeExtend({
2254
+ retired: z.boolean().optional(),
2255
+ id: z.string().regex(/^codex\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/),
2256
+ upstreamModelId: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/),
2257
+ capabilities: ModelCapabilitiesV1Schema,
2258
+ pricing: z.never().optional(),
2259
+ })
2260
+ .strict()
2261
+ .superRefine((model, context) => {
2262
+ if (model.id !== `${CODEX_MODEL_ID_PREFIX}${model.upstreamModelId}`) {
2263
+ context.addIssue({
2264
+ code: "custom",
2265
+ path: ["id"],
2266
+ message: "Codex product id must match its upstream model slug",
2267
+ });
2268
+ }
2269
+ });
2270
+
2247
2271
  export const ModelCatalogDocument = z
2248
2272
  .object({
2249
2273
  schemaVersion: z.literal(1),
@@ -2251,8 +2275,9 @@ export const ModelCatalogDocument = z
2251
2275
  * fallback for existing documents; operators should set this explicitly
2252
2276
  * when cutting over a registry or connected-subscription default. */
2253
2277
  defaultModel: z.string().min(1).optional(),
2254
- builtInModels: z.array(z.string().min(1)).min(1),
2278
+ builtInModels: z.array(z.string().min(1)),
2255
2279
  registryProviders: z.array(DeploymentRegistryProviderSchema).default([]),
2280
+ codexModels: z.array(CodexCatalogModelSchema).optional(),
2256
2281
  gatewayModels: z.array(DeploymentGatewayCatalogModelSchema).default([]),
2257
2282
  openrouterModels: z.array(OpenRouterCatalogModel).default([]),
2258
2283
  modelNotes: z.record(z.string().min(1), ModelNote).default({}),
@@ -2263,6 +2288,13 @@ export const ModelCatalogDocument = z
2263
2288
  })
2264
2289
  .strict()
2265
2290
  .superRefine((document, context) => {
2291
+ if (document.builtInModels.length === 0 && !document.defaultModel) {
2292
+ context.addIssue({
2293
+ code: "custom",
2294
+ path: ["defaultModel"],
2295
+ message: "catalogs without built-in models require an explicit default model",
2296
+ });
2297
+ }
2266
2298
  const productIds = new Set<string>();
2267
2299
  const providerIds = new Set<string>();
2268
2300
  const gatewayUpstreamIds = new Set<string>();
@@ -2284,6 +2316,16 @@ export const ModelCatalogDocument = z
2284
2316
  productIds.add(id);
2285
2317
  };
2286
2318
  document.builtInModels.forEach((id, index) => add(id, ["builtInModels", index]));
2319
+ document.codexModels?.forEach((model, index) => add(model.id, ["codexModels", index, "id"]));
2320
+ if (
2321
+ document.codexModels?.some((model) => model.retired && model.id === document.defaultModel)
2322
+ ) {
2323
+ context.addIssue({
2324
+ code: "custom",
2325
+ path: ["defaultModel"],
2326
+ message: "a retired model cannot be the deployment default",
2327
+ });
2328
+ }
2287
2329
  document.registryProviders.forEach((provider, providerIndex) => {
2288
2330
  if (RESERVED_MODEL_PROVIDER_IDS.has(provider.id)) {
2289
2331
  context.addIssue({
@@ -2333,7 +2375,10 @@ export const ModelCatalogDocument = z
2333
2375
  if (
2334
2376
  document.defaultModel &&
2335
2377
  !productIds.has(document.defaultModel) &&
2336
- !document.defaultModel.startsWith(CODEX_MODEL_ID_PREFIX) &&
2378
+ !(
2379
+ document.codexModels === undefined &&
2380
+ document.defaultModel.startsWith(CODEX_MODEL_ID_PREFIX)
2381
+ ) &&
2337
2382
  !document.defaultModel.startsWith(XAI_SUBSCRIPTION_MODEL_ID_PREFIX)
2338
2383
  ) {
2339
2384
  context.addIssue({
@@ -2418,6 +2463,8 @@ export function applyModelCatalogDocument(settings: Settings, rawDocument: unkno
2418
2463
  ),
2419
2464
  resolvedGatewayModelsJson: JSON.stringify(document.gatewayModels),
2420
2465
  resolvedOpenRouterModelsJson: JSON.stringify(document.openrouterModels),
2466
+ resolvedCodexModelsJson:
2467
+ document.codexModels === undefined ? undefined : JSON.stringify(document.codexModels),
2421
2468
  modelNotesJson: JSON.stringify(document.modelNotes),
2422
2469
  };
2423
2470
  return resolved;
@@ -2701,6 +2748,73 @@ export function configuredOpenRouterOrganizationProductModelIds(settings: Settin
2701
2748
  * llm-prices.com as a ground-truth canary; it does not generate this table.
2702
2749
  */
2703
2750
  export const defaultModelPricing: Record<string, ModelPricingScheduleV1> = {
2751
+ "gpt-6-astra": {
2752
+ default: {
2753
+ // OpenAI list price: $10 / $1 cached / $12.50 cache write / $50 output.
2754
+ inputMicrosPerMillionTokens: 10_000_000,
2755
+ cachedInputMicrosPerMillionTokens: 1_000_000,
2756
+ cacheWriteMicrosPerMillionTokens: 12_500_000,
2757
+ outputMicrosPerMillionTokens: 50_000_000,
2758
+ marginBps: 500,
2759
+ },
2760
+ inputTokenTiers: [
2761
+ {
2762
+ // Prompts with more than 272K input tokens use 2x input and 1.5x output.
2763
+ minimumInputTokens: 272_001,
2764
+ pricing: {
2765
+ inputMicrosPerMillionTokens: 20_000_000,
2766
+ cachedInputMicrosPerMillionTokens: 2_000_000,
2767
+ cacheWriteMicrosPerMillionTokens: 25_000_000,
2768
+ outputMicrosPerMillionTokens: 75_000_000,
2769
+ marginBps: 500,
2770
+ },
2771
+ },
2772
+ ],
2773
+ },
2774
+ "gpt-6-sol": {
2775
+ default: {
2776
+ // OpenAI list price: $2 / $0.20 cached / $2.50 cache write / $10 output.
2777
+ inputMicrosPerMillionTokens: 2_000_000,
2778
+ cachedInputMicrosPerMillionTokens: 200_000,
2779
+ cacheWriteMicrosPerMillionTokens: 2_500_000,
2780
+ outputMicrosPerMillionTokens: 10_000_000,
2781
+ marginBps: 500,
2782
+ },
2783
+ inputTokenTiers: [
2784
+ {
2785
+ minimumInputTokens: 272_001,
2786
+ pricing: {
2787
+ inputMicrosPerMillionTokens: 4_000_000,
2788
+ cachedInputMicrosPerMillionTokens: 400_000,
2789
+ cacheWriteMicrosPerMillionTokens: 5_000_000,
2790
+ outputMicrosPerMillionTokens: 15_000_000,
2791
+ marginBps: 500,
2792
+ },
2793
+ },
2794
+ ],
2795
+ },
2796
+ "gpt-6-luna": {
2797
+ default: {
2798
+ // OpenAI list price: $0.10 / $0.01 cached / $0.125 cache write / $0.50 output.
2799
+ inputMicrosPerMillionTokens: 100_000,
2800
+ cachedInputMicrosPerMillionTokens: 10_000,
2801
+ cacheWriteMicrosPerMillionTokens: 125_000,
2802
+ outputMicrosPerMillionTokens: 500_000,
2803
+ marginBps: 500,
2804
+ },
2805
+ inputTokenTiers: [
2806
+ {
2807
+ minimumInputTokens: 272_001,
2808
+ pricing: {
2809
+ inputMicrosPerMillionTokens: 200_000,
2810
+ cachedInputMicrosPerMillionTokens: 20_000,
2811
+ cacheWriteMicrosPerMillionTokens: 250_000,
2812
+ outputMicrosPerMillionTokens: 750_000,
2813
+ marginBps: 500,
2814
+ },
2815
+ },
2816
+ ],
2817
+ },
2704
2818
  "gpt-5.6-sol": {
2705
2819
  default: {
2706
2820
  // Promotional OpenAI pricing, guaranteed through at least 2026-11-21.
@@ -2981,6 +3095,7 @@ export function getSettings(source: NodeJS.ProcessEnv = process.env): Settings {
2981
3095
  environmentsEncryptionKey: optional("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"),
2982
3096
  integrationsEnabled: optional("OPENGENI_INTEGRATIONS_ENABLED"),
2983
3097
  integrationsStateSecret: optional("OPENGENI_INTEGRATIONS_STATE_SECRET"),
3098
+ integrationsOauthShortStateEnabled: optional("OPENGENI_INTEGRATIONS_OAUTH_SHORT_STATE_ENABLED"),
2984
3099
  integrationsAllowPrivateNetworkTargets: optional(
2985
3100
  "OPENGENI_INTEGRATIONS_ALLOW_PRIVATE_NETWORK_TARGETS",
2986
3101
  ),
@@ -4273,8 +4388,8 @@ export function productShortLabelForModelId(modelId: string): string | null {
4273
4388
  ? modelId.slice(CODEX_MODEL_ID_PREFIX.length)
4274
4389
  : modelId;
4275
4390
  switch (slug) {
4276
- case "grok-4.6":
4277
- return "4.6";
4391
+ case "grok-4.7":
4392
+ return "4.7";
4278
4393
  case "gpt-5.6-sol":
4279
4394
  return "5.6 Sol";
4280
4395
  case "gpt-5.6-terra":
@@ -4283,6 +4398,10 @@ export function productShortLabelForModelId(modelId: string): string | null {
4283
4398
  return "5.6 Luna";
4284
4399
  case "gpt-6-astra":
4285
4400
  return "6 Astra";
4401
+ case "gpt-6-sol":
4402
+ return "6 Sol";
4403
+ case "gpt-6-luna":
4404
+ return "6 Luna";
4286
4405
  default:
4287
4406
  return null;
4288
4407
  }
@@ -4320,8 +4439,9 @@ function builtinLatencyModesForModel(modelId: string): Array<{
4320
4439
  }> {
4321
4440
  if (
4322
4441
  isBuiltinGpt56ModelId(modelId) ||
4442
+ modelId.startsWith("gpt-6-") ||
4323
4443
  modelId.startsWith("codex/gpt-5.6-") ||
4324
- modelId === "codex/gpt-6-astra"
4444
+ modelId.startsWith("codex/gpt-6-")
4325
4445
  ) {
4326
4446
  return [
4327
4447
  { id: "standard", upstream: "supported", runnable: true },
@@ -4342,7 +4462,7 @@ function builtinPromptCachingForModel(
4342
4462
  const slug = modelId.startsWith(CODEX_MODEL_ID_PREFIX)
4343
4463
  ? modelId.slice(CODEX_MODEL_ID_PREFIX.length)
4344
4464
  : modelId;
4345
- return slug.startsWith("gpt-5.6-")
4465
+ return slug.startsWith("gpt-5.6-") || slug.startsWith("gpt-6-")
4346
4466
  ? { upstream: "supported", runnable: true, mode: "implicit" }
4347
4467
  : undefined;
4348
4468
  }
@@ -4352,7 +4472,7 @@ function builtinHostedImageGenerationForModel(settings: Settings, modelId: strin
4352
4472
  return (
4353
4473
  settings.openaiProvider === "openai" &&
4354
4474
  isDirectOpenAiApiBaseUrl(settings.openaiBaseUrl) &&
4355
- isBuiltinGpt56ModelId(modelId)
4475
+ (isBuiltinGpt56ModelId(modelId) || modelId.startsWith("gpt-6-"))
4356
4476
  );
4357
4477
  }
4358
4478
 
@@ -4650,12 +4770,13 @@ export function configuredProviders(
4650
4770
  if (settings.openaiProvider === "azure") {
4651
4771
  const baseUrl = settings.azureOpenaiBaseUrl ?? settings.azureOpenaiEndpoint;
4652
4772
  builtin.baseUrl = baseUrl ? normalizeRegistryBaseUrl(baseUrl, builtin.id) : undefined;
4653
- builtin.apiKey = settings.azureOpenaiApiKey ?? settings.azureOpenaiAdToken;
4773
+ builtin.apiKey =
4774
+ usableDeploymentSecret(settings.azureOpenaiApiKey) ?? settings.azureOpenaiAdToken;
4654
4775
  } else {
4655
4776
  builtin.baseUrl = settings.openaiBaseUrl
4656
4777
  ? normalizeRegistryBaseUrl(settings.openaiBaseUrl, builtin.id)
4657
4778
  : undefined;
4658
- builtin.apiKey = settings.openaiApiKey;
4779
+ builtin.apiKey = usableDeploymentSecret(settings.openaiApiKey);
4659
4780
  }
4660
4781
  const registry = configuredRegistryProviders(settings).map(
4661
4782
  (provider): ResolvedModelProvider => ({
@@ -4689,6 +4810,15 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
4689
4810
  if (providers.some((provider) => provider.id === CODEX_PROVIDER_ID)) {
4690
4811
  return settings;
4691
4812
  }
4813
+ const catalogModels =
4814
+ settings.resolvedCodexModelsJson === undefined
4815
+ ? undefined
4816
+ : z
4817
+ .array(CodexCatalogModelSchema)
4818
+ .parse(JSON.parse(settings.resolvedCodexModelsJson))
4819
+ .filter((model) => !model.retired)
4820
+ .map(({ retired: _retired, ...model }) => model);
4821
+ if (catalogModels?.length === 0) return settings;
4692
4822
  const provider: RegistryProvider = {
4693
4823
  kind: "codex-subscription",
4694
4824
  id: CODEX_PROVIDER_ID,
@@ -4696,40 +4826,42 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
4696
4826
  api: "responses",
4697
4827
  wireProfile: "openai",
4698
4828
  baseUrl: CODEX_PROVIDER_BASE_URL,
4699
- models: CODEX_FALLBACK_MODEL_SLUGS.map((slug) => {
4700
- const capabilities = {
4701
- ...legacyModelCapabilities(settings, {
4829
+ models:
4830
+ catalogModels ??
4831
+ CODEX_FALLBACK_MODEL_SLUGS.map((slug) => {
4832
+ const capabilities = {
4833
+ ...legacyModelCapabilities(settings, {
4834
+ reasoningEffort: true,
4835
+ hostedWebSearch: true,
4836
+ vision: slug.startsWith("gpt-5.6-") || slug.startsWith("gpt-6-"),
4837
+ }),
4838
+ ...(builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)
4839
+ ? {
4840
+ promptCaching: builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)!,
4841
+ }
4842
+ : {}),
4843
+ latencyModes: builtinLatencyModesForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`),
4844
+ };
4845
+ return {
4846
+ id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
4847
+ upstreamModelId: slug,
4848
+ label: productLabelForModelId(slug),
4849
+ ...(productShortLabelForModelId(slug)
4850
+ ? { shortLabel: productShortLabelForModelId(slug)! }
4851
+ : {}),
4702
4852
  reasoningEffort: true,
4853
+ // The ChatGPT/Codex Responses backend accepts the native web_search
4854
+ // hosted tool (unlike hosted apply_patch/computer transports). Declaring
4855
+ // this here makes provider resolution truthful; the worker still applies
4856
+ // the durable session/turn policy gate before attaching it.
4703
4857
  hostedWebSearch: true,
4704
- vision: slug.startsWith("gpt-5.6-") || slug === "gpt-6-astra",
4705
- }),
4706
- ...(builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)
4707
- ? {
4708
- promptCaching: builtinPromptCachingForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`)!,
4709
- }
4710
- : {}),
4711
- latencyModes: builtinLatencyModesForModel(`${CODEX_MODEL_ID_PREFIX}${slug}`),
4712
- };
4713
- return {
4714
- id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
4715
- upstreamModelId: slug,
4716
- label: productLabelForModelId(slug),
4717
- ...(productShortLabelForModelId(slug)
4718
- ? { shortLabel: productShortLabelForModelId(slug)! }
4719
- : {}),
4720
- reasoningEffort: true,
4721
- // The ChatGPT/Codex Responses backend accepts the native web_search
4722
- // hosted tool (unlike hosted apply_patch/computer transports). Declaring
4723
- // this here makes provider resolution truthful; the worker still applies
4724
- // the durable session/turn policy gate before attaching it.
4725
- hostedWebSearch: true,
4726
- capabilities,
4727
- contextWindowTokens: CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
4728
- effectiveContextWindowTokens: CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
4729
- autoCompactTokenLimit: CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
4730
- toolOutputTruncationTokens: CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS,
4731
- };
4732
- }),
4858
+ capabilities,
4859
+ contextWindowTokens: CODEX_MODEL_CONTEXT_WINDOW_TOKENS,
4860
+ effectiveContextWindowTokens: CODEX_MODEL_EFFECTIVE_CONTEXT_WINDOW_TOKENS,
4861
+ autoCompactTokenLimit: CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
4862
+ toolOutputTruncationTokens: CODEX_MODEL_TOOL_OUTPUT_TRUNCATION_TOKENS,
4863
+ };
4864
+ }),
4733
4865
  };
4734
4866
  return {
4735
4867
  ...settings,
@@ -4968,7 +5100,7 @@ export function configuredModels(
4968
5100
  reasoningEffort: true,
4969
5101
  hostedWebSearch: settings.webSearchEnabled,
4970
5102
  hostedImageGeneration: builtinHostedImageGenerationForModel(settings, id),
4971
- vision: id.startsWith("gpt-5.6-"),
5103
+ vision: id.startsWith("gpt-5.6-") || id.startsWith("gpt-6-"),
4972
5104
  }),
4973
5105
  ...(builtinPromptCachingForModel(id)
4974
5106
  ? { promptCaching: builtinPromptCachingForModel(id)! }
@@ -5079,7 +5211,78 @@ export function canonicalizeConfiguredModelId(settings: Settings, modelId: strin
5079
5211
  * openai allow-list, then registry ids.
5080
5212
  */
5081
5213
  export function configuredAllowedModels(settings: Settings): string[] {
5082
- return configuredModels(settings).map((model) => model.id);
5214
+ return configuredModels(settings)
5215
+ .filter((model) => isModelAvailableForNewSelection(settings, model.id))
5216
+ .map((model) => model.id);
5217
+ }
5218
+
5219
+ /** Retirement is selection metadata, never provider authority or executable identity. */
5220
+ export function isModelAvailableForNewSelection(settings: Settings, modelId: string): boolean {
5221
+ if (!modelId.startsWith(CODEX_MODEL_ID_PREFIX) || settings.resolvedCodexModelsJson === undefined)
5222
+ return true;
5223
+ const models = z
5224
+ .array(CodexCatalogModelSchema)
5225
+ .parse(JSON.parse(settings.resolvedCodexModelsJson));
5226
+ return models.some((model) => model.id === modelId && !model.retired);
5227
+ }
5228
+
5229
+ /** Called only with the durable policy installed for a claimed accepted attempt. */
5230
+ export function settingsForAcceptedSubscriptionTurn(
5231
+ settings: Settings,
5232
+ policy: TurnExecutionPolicyV1,
5233
+ expected: {
5234
+ modelId: string;
5235
+ reasoningEffort: Settings["openaiReasoningEffort"];
5236
+ latencyMode?: LatencyMode;
5237
+ },
5238
+ ): Settings {
5239
+ const parsed = TurnExecutionPolicyV1.parse(policy);
5240
+ if (!parsed.productModelId.startsWith(CODEX_MODEL_ID_PREFIX)) return settings;
5241
+ if (!settings.codexSubscriptionEnabled) throw new Error("Codex subscription is disabled");
5242
+ const models =
5243
+ settings.resolvedCodexModelsJson === undefined
5244
+ ? []
5245
+ : z.array(CodexCatalogModelSchema).parse(JSON.parse(settings.resolvedCodexModelsJson));
5246
+ const retained = models.find((model) => model.id === parsed.productModelId && model.retired);
5247
+ if (!retained) {
5248
+ assertTurnExecutionPolicyMatchesConfigV1(settings, parsed, expected);
5249
+ return settings;
5250
+ }
5251
+ const { retired: _retired, ...model } = retained;
5252
+ // Use the existing fixed broker transport. Only this exact definition is added;
5253
+ // selection metadata remains retired in the original catalog JSON.
5254
+ const withoutCodex = {
5255
+ ...settings,
5256
+ modelProvidersJson: JSON.stringify(
5257
+ parseModelProvidersJson(settings.modelProvidersJson).filter(
5258
+ (provider) => provider.id !== CODEX_PROVIDER_ID,
5259
+ ),
5260
+ ),
5261
+ };
5262
+ const providerSettings = withCodexCatalogProvider({
5263
+ ...withoutCodex,
5264
+ resolvedCodexModelsJson: JSON.stringify([model]),
5265
+ });
5266
+ const retainedProvider = parseModelProvidersJson(providerSettings.modelProvidersJson).find(
5267
+ (provider) => provider.id === CODEX_PROVIDER_ID,
5268
+ )!;
5269
+ const providers = parseModelProvidersJson(settings.modelProvidersJson);
5270
+ const current = providers.find((provider) => provider.id === CODEX_PROVIDER_ID);
5271
+ const restored = {
5272
+ ...settings,
5273
+ modelProvidersJson: JSON.stringify([
5274
+ ...providers.filter((provider) => provider.id !== CODEX_PROVIDER_ID),
5275
+ {
5276
+ ...retainedProvider,
5277
+ models: [
5278
+ ...(current?.models ?? []).filter((candidate) => candidate.id !== model.id),
5279
+ model,
5280
+ ],
5281
+ },
5282
+ ]),
5283
+ };
5284
+ assertTurnExecutionPolicyMatchesConfigV1(restored, parsed, expected);
5285
+ return restored;
5083
5286
  }
5084
5287
 
5085
5288
  /**
@@ -5184,6 +5387,9 @@ export function resolveTurnExecutionPolicyV1(
5184
5387
  ): TurnExecutionPolicyV1 {
5185
5388
  const catalogSettings = settingsForTurnExecutionPolicy(settings, input.modelId);
5186
5389
  const productModelId = canonicalizeConfiguredModelId(catalogSettings, input.modelId);
5390
+ if (!isModelAvailableForNewSelection(catalogSettings, productModelId)) {
5391
+ throw new Error("Turn execution policy model is retired from new selection");
5392
+ }
5187
5393
  const resolved = resolveModelProvider(catalogSettings, productModelId);
5188
5394
  if (!resolved) {
5189
5395
  throw new Error("Turn execution policy model is not present in the configured catalog");
@@ -7073,6 +7279,13 @@ export function validateModelCatalogSettings(
7073
7279
  models.filter((model) => model.credentialSource.kind === "deployment").map((model) => model.id),
7074
7280
  );
7075
7281
  const noteProductIds = new Set(models.map((model) => model.id));
7282
+ if (settings.resolvedCodexModelsJson !== undefined) {
7283
+ for (const model of z
7284
+ .array(CodexCatalogModelSchema)
7285
+ .parse(JSON.parse(settings.resolvedCodexModelsJson))) {
7286
+ noteProductIds.add(model.id);
7287
+ }
7288
+ }
7076
7289
  for (const model of configuredGatewayCatalogModels(settings)) {
7077
7290
  deploymentProductIds.add(model.productId);
7078
7291
  noteProductIds.add(model.productId);