@gajae-code/coding-agent 0.12.21 → 0.13.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.
Files changed (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -6,13 +6,16 @@ import {
6
6
  type AuthCredentialSelector,
7
7
  applyFinalCodexGpt56ContextCap,
8
8
  type CacheRetention,
9
+ CODEX_GPT_5_6_CONTEXT_CAP,
9
10
  type Context,
11
+ codexContextOverrideKey,
10
12
  createModelManager,
11
13
  enrichModelThinking,
12
14
  getBundledModels,
13
15
  getBundledProviders,
14
16
  googleAntigravityModelManagerOptions,
15
17
  googleGeminiCliModelManagerOptions,
18
+ isCodexGpt56Tier,
16
19
  isKnownProvider,
17
20
  type Model,
18
21
  type ModelManagerOptions,
@@ -76,8 +79,15 @@ import {
76
79
  type ProviderAuthMode,
77
80
  type ProviderDiscovery,
78
81
  } from "./models-config-schema";
82
+ import {
83
+ buildProviderSelectionCatalog,
84
+ createProviderSelectionPolicy,
85
+ type EffectiveProviderAuth,
86
+ type ProviderSelectionPolicy,
87
+ } from "./provider-selection-policy";
79
88
  import { type Settings, settings } from "./settings";
80
89
 
90
+ export type { EffectiveProviderAuth, ProviderSelectionPolicy } from "./provider-selection-policy";
81
91
  export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
82
92
 
83
93
  export { isAuthenticated, kNoAuth };
@@ -158,6 +168,7 @@ export const GJC_MODEL_ASSIGNMENT_TARGETS: Record<GjcModelAssignmentTargetId, Gj
158
168
  settingsPath: "task.agentModelOverrides",
159
169
  },
160
170
  critic: { id: "critic", tag: "CRITIC", name: "Critic", color: "error", settingsPath: "task.agentModelOverrides" },
171
+ image: { id: "image", tag: "IMAGE", name: "Image", color: "accent", settingsPath: "modelRoles" },
161
172
  };
162
173
 
163
174
  /** Alias for ModelRoleInfo - used for both built-in and custom roles */
@@ -651,6 +662,8 @@ export interface CanonicalModelQueryOptions {
651
662
  candidates?: readonly Model<Api>[];
652
663
  /** Stable session identity used to keep a canonical variant sticky within a session. */
653
664
  sessionId?: string;
665
+ /** Credential-selection session used to classify effective provider auth. Defaults to sessionId. */
666
+ credentialSessionId?: string;
654
667
  }
655
668
 
656
669
  /** Result of loading custom models from models.json */
@@ -851,7 +864,18 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
851
864
  if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
852
865
  if (override.output !== undefined) result.output = override.output as ("text" | "image")[];
853
866
  if (override.cacheRetention !== undefined) result.cacheRetention = override.cacheRetention;
854
- if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
867
+ const contextWindowOverride = toPositiveNumberOrUndefined(override.contextWindow);
868
+ if (contextWindowOverride !== undefined) {
869
+ result.contextWindow = contextWindowOverride;
870
+ } else if (override.contextWindow !== undefined && !isCodexGpt56Tier({ id: model.id })) {
871
+ // Codex-tier invalid overrides are diagnosed in #collectCodexContextWindowOverrides;
872
+ // every other provider is diagnosed here so an ignored override is never silent.
873
+ logger.warn("model context-window override ignored: value must be a positive finite number", {
874
+ model: model.id,
875
+ provider: model.provider,
876
+ override: override.contextWindow,
877
+ });
878
+ }
855
879
  if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
856
880
  if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
857
881
  if (override.wireModelId !== undefined) result.wireModelId = override.wireModelId;
@@ -871,6 +895,26 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
871
895
  result.compat = mergeCompat(model.compat, override.compat);
872
896
  return enrichModelThinking(result);
873
897
  }
898
+ /**
899
+ * Normalizes `modelOverrides` keys to lowercase so override matching is
900
+ * case-insensitive everywhere (the Codex cap exemption is keyed by
901
+ * `codexContextOverrideKey`, which lowercases both sides). Without this,
902
+ * a mixed-case config key is exempted from the cap without its value ever
903
+ * being merged into the model.
904
+ */
905
+ function normalizeModelOverrideKeys(
906
+ modelOverrides: Map<string, Map<string, ModelOverride>>,
907
+ ): Map<string, Map<string, ModelOverride>> {
908
+ const normalized = new Map<string, Map<string, ModelOverride>>();
909
+ for (const [provider, perModel] of modelOverrides) {
910
+ const perProvider = new Map<string, ModelOverride>();
911
+ for (const [modelId, override] of perModel) {
912
+ perProvider.set(modelId.toLowerCase(), override);
913
+ }
914
+ normalized.set(provider.toLowerCase(), perProvider);
915
+ }
916
+ return normalized;
917
+ }
874
918
 
875
919
  interface CustomModelDefinitionLike {
876
920
  id: string;
@@ -1149,7 +1193,8 @@ function getDisabledProviderIdsFromSettings(): Set<string> {
1149
1193
 
1150
1194
  function getConfiguredProviderOrderFromSettings(): string[] {
1151
1195
  try {
1152
- return settings.get("modelProviderOrder");
1196
+ const configured = settings.getGlobal("modelProviderOrder");
1197
+ return Array.isArray(configured) ? configured.filter((value): value is string => typeof value === "string") : [];
1153
1198
  } catch {
1154
1199
  return [];
1155
1200
  }
@@ -1178,7 +1223,12 @@ interface ModelManagerDiscoveryOptions {
1178
1223
  */
1179
1224
  export class ModelRegistry {
1180
1225
  #models: Model<Api>[] = [];
1181
- #canonicalIndex: CanonicalModelIndex = { records: [], byId: new Map(), bySelector: new Map() };
1226
+ #canonicalIndex: CanonicalModelIndex = {
1227
+ records: [],
1228
+ byId: new Map(),
1229
+ bySelector: new Map(),
1230
+ aliases: new Map(),
1231
+ };
1182
1232
  #availableModelsCache: Model<Api>[] | undefined;
1183
1233
  #availableModelsDisabledProviders: string | undefined;
1184
1234
  #availableModelsEnvFingerprint: string | undefined;
@@ -1205,6 +1255,7 @@ export class ModelRegistry {
1205
1255
  #customModelOverlays: CustomModelOverlay[] = [];
1206
1256
  #providerOverrides: Map<string, ProviderOverride> = new Map();
1207
1257
  #modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
1258
+ #codexContextWindowOverrides: Map<string, number> = new Map();
1208
1259
  #equivalenceConfig: ModelEquivalenceConfig | undefined;
1209
1260
  #modelBindingsApplier = new ModelBindingsApplier();
1210
1261
  #modelProfiles: Map<string, ModelProfileDefinition> = mergeModelProfiles();
@@ -1345,17 +1396,20 @@ export class ModelRegistry {
1345
1396
  // removed from models.yml must actually disappear from the resolver, not
1346
1397
  // linger from the previous parse. The post-load setters below repopulate.
1347
1398
  this.authStorage.clearConfigApiKeys();
1348
- // Restore runtime API keys before #loadModels — survives because
1349
- // #loadModels only calls .set() on #customProviderApiKeys, never reassigns it.
1350
- for (const [k, v] of this.#runtimeProviderApiKeys) {
1351
- this.#customProviderApiKeys.set(k, v);
1352
- }
1399
+ // Runtime provider keys are reapplied after #loadModels so they retain
1400
+ // registration-time precedence over colliding static provider keys.
1353
1401
  this.#providerOverrides.clear();
1354
1402
  this.#modelOverrides.clear();
1355
1403
  this.#equivalenceConfig = undefined;
1356
1404
  this.#modelBindingsApplier.setBindings(undefined);
1357
1405
  this.#configError = undefined;
1358
1406
  this.#loadModels();
1407
+ for (const [provider, apiKeyConfig] of this.#runtimeProviderApiKeys) {
1408
+ const resolved = resolveApiKeyConfig(apiKeyConfig);
1409
+ if (!resolved) continue;
1410
+ this.#customProviderApiKeys.set(provider, resolved);
1411
+ this.authStorage.setConfigApiKey(provider, resolved);
1412
+ }
1359
1413
  this.#lastDisabledProviderKey = disabledProviderKey;
1360
1414
  }
1361
1415
 
@@ -1387,7 +1441,8 @@ export class ModelRegistry {
1387
1441
  this.#configuredDiscoveryProviderIds = new Set(discoverableProviders.map(provider => provider.provider));
1388
1442
  this.#customModelOverlays = customModels;
1389
1443
  this.#providerOverrides = overrides;
1390
- this.#modelOverrides = modelOverrides;
1444
+ this.#modelOverrides = normalizeModelOverrideKeys(modelOverrides);
1445
+ this.#codexContextWindowOverrides = this.#collectCodexContextWindowOverrides();
1391
1446
  this.#equivalenceConfig = equivalence;
1392
1447
  this.#modelBindingsApplier.setBindings(modelBindings);
1393
1448
  this.#modelProfiles = mergeModelProfiles(profiles);
@@ -1404,7 +1459,11 @@ export class ModelRegistry {
1404
1459
  // Merge runtime extension models so they survive refresh() cycles
1405
1460
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1406
1461
  const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
1407
- this.#models = applyFinalCodexGpt56ContextCap(this.#applyRuntimeProviderOverrides(withModelOverrides));
1462
+ this.#models = applyFinalCodexGpt56ContextCap(
1463
+ this.#applyRuntimeProviderOverrides(withModelOverrides),
1464
+ undefined,
1465
+ this.#codexContextWindowOverrides,
1466
+ );
1408
1467
  this.#rebuildProviderActivity();
1409
1468
  this.#rebuildCanonicalIndex();
1410
1469
  this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
@@ -2161,7 +2220,11 @@ export class ModelRegistry {
2161
2220
  // Merge runtime extension models so they survive online discovery completion
2162
2221
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
2163
2222
  const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
2164
- this.#models = applyFinalCodexGpt56ContextCap(this.#applyRuntimeProviderOverrides(withModelOverrides));
2223
+ this.#models = applyFinalCodexGpt56ContextCap(
2224
+ this.#applyRuntimeProviderOverrides(withModelOverrides),
2225
+ undefined,
2226
+ this.#codexContextWindowOverrides,
2227
+ );
2165
2228
  this.#rebuildCanonicalIndex();
2166
2229
  }
2167
2230
 
@@ -3007,10 +3070,10 @@ export class ModelRegistry {
3007
3070
  }
3008
3071
 
3009
3072
  #applyProviderModelOverrides(provider: string, models: Model<Api>[]): Model<Api>[] {
3010
- const overrides = this.#modelOverrides.get(provider);
3073
+ const overrides = this.#modelOverrides.get(provider.toLowerCase());
3011
3074
  if (!overrides || overrides.size === 0) return models;
3012
3075
  return models.map(model => {
3013
- const override = overrides.get(model.id);
3076
+ const override = overrides.get(model.id.toLowerCase());
3014
3077
  if (!override) return model;
3015
3078
  return applyModelOverride(model, override);
3016
3079
  });
@@ -3098,7 +3161,7 @@ export class ModelRegistry {
3098
3161
  }
3099
3162
  #applyRuntimeProviderOverride(model: Model<Api>, override: ProviderOverride): Model<Api> {
3100
3163
  const withTransportOverride = this.#applyProviderTransportOverride(model, override);
3101
- const modelCompat = this.#modelOverrides.get(model.provider)?.get(model.id)?.compat;
3164
+ const modelCompat = this.#modelOverrides.get(model.provider.toLowerCase())?.get(model.id.toLowerCase())?.compat;
3102
3165
  return modelCompat
3103
3166
  ? { ...withTransportOverride, compat: mergeCompat(withTransportOverride.compat, modelCompat) }
3104
3167
  : withTransportOverride;
@@ -3111,12 +3174,52 @@ export class ModelRegistry {
3111
3174
  return this.#applyRuntimeProviderOverride(model, override);
3112
3175
  });
3113
3176
  }
3177
+ /**
3178
+ * Collects explicit user `contextWindow` overrides keyed by provider + model
3179
+ * id (`codexContextOverrideKey`), and surfaces diagnostics when one applies
3180
+ * to a Codex GPT-5.6 tier
3181
+ * model. The map is computed once per config load and feeds the final
3182
+ * context cap so an explicit override is never silently re-clamped.
3183
+ */
3184
+ #collectCodexContextWindowOverrides(): Map<string, number> {
3185
+ const result = new Map<string, number>();
3186
+ for (const [provider, providerOverrides] of this.#modelOverrides) {
3187
+ for (const [modelId, override] of providerOverrides) {
3188
+ if (override.contextWindow === undefined) {
3189
+ continue;
3190
+ }
3191
+ const normalizedId = modelId.toLowerCase();
3192
+ const value = toPositiveNumberOrUndefined(override.contextWindow);
3193
+ if (value === undefined) {
3194
+ if (isCodexGpt56Tier({ id: normalizedId })) {
3195
+ logger.warn("codex gpt-5.6 context-window override ignored: value must be a positive finite number", {
3196
+ model: modelId,
3197
+ provider,
3198
+ override: override.contextWindow,
3199
+ });
3200
+ }
3201
+ continue;
3202
+ }
3203
+ result.set(codexContextOverrideKey(provider, modelId), value);
3204
+ if (!isCodexGpt56Tier({ id: normalizedId })) {
3205
+ continue;
3206
+ }
3207
+ logger.warn("codex gpt-5.6 context-window override active; verify it against the live product limit", {
3208
+ model: modelId,
3209
+ provider,
3210
+ override: override.contextWindow,
3211
+ enforced: CODEX_GPT_5_6_CONTEXT_CAP.enforced,
3212
+ });
3213
+ }
3214
+ }
3215
+ return result;
3216
+ }
3114
3217
  #applyModelOverrides(models: Model<Api>[], overrides: Map<string, Map<string, ModelOverride>>): Model<Api>[] {
3115
3218
  if (overrides.size === 0) return models;
3116
3219
  return models.map(model => {
3117
- const providerOverrides = overrides.get(model.provider);
3220
+ const providerOverrides = overrides.get(model.provider.toLowerCase());
3118
3221
  if (!providerOverrides) return model;
3119
- const override = providerOverrides.get(model.id);
3222
+ const override = providerOverrides.get(model.id.toLowerCase());
3120
3223
  if (!override) return model;
3121
3224
  return applyModelOverride(model, override);
3122
3225
  });
@@ -3129,7 +3232,7 @@ export class ModelRegistry {
3129
3232
  if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.provider === "jetbrains-junie") {
3130
3233
  return model;
3131
3234
  }
3132
- const overrides = this.#modelOverrides.get(model.provider)?.get(model.id);
3235
+ const overrides = this.#modelOverrides.get(model.provider.toLowerCase())?.get(model.id.toLowerCase());
3133
3236
  if (!overrides) {
3134
3237
  return applyModelOverride(model, { contextWindow: 1_000_000 });
3135
3238
  }
@@ -3245,54 +3348,113 @@ export class ModelRegistry {
3245
3348
  });
3246
3349
  }
3247
3350
 
3248
- #providerRank(models: readonly Model<Api>[]): Map<string, number> {
3249
- const configuredProviders = getConfiguredProviderOrderFromSettings();
3250
- const result = new Map<string, number>();
3251
- let nextRank = 0;
3252
- for (const provider of configuredProviders) {
3253
- const normalized = provider.trim().toLowerCase();
3254
- if (!normalized || result.has(normalized)) {
3351
+ /**
3352
+ * Effective credential provenance for a provider, derived from existing
3353
+ * AuthStorage/session credential surfaces — never from token shape.
3354
+ *
3355
+ * Precedence mirrors actual credential selection (`getApiKey`):
3356
+ * 1. Runtime/config API-key overrides (the actual credential resolver's
3357
+ * highest-priority surfaces)
3358
+ * 2. Session-recorded stored credential type
3359
+ * 3. Stored/custom API-key surfaces
3360
+ * 4. OAuth, only when it is the effective remaining stored credential
3361
+ * 5. Keyless providers
3362
+ * 6. Unknown (no credential surface)
3363
+ */
3364
+ #effectiveProviderAuth(provider: string, sessionId?: string): EffectiveProviderAuth {
3365
+ const credentialType = this.authStorage.getEffectiveCredentialType(provider, sessionId);
3366
+ if (credentialType === "api_key") return "key";
3367
+ if (credentialType === "oauth") return "oauth";
3368
+ if (this.#keylessProviders.has(provider)) return "keyless";
3369
+ return "unknown";
3370
+ }
3371
+
3372
+ /**
3373
+ * Build the pure provider selection policy from the registry catalog order,
3374
+ * configured `modelProviderOrder`, and effective credential provenance for
3375
+ * the given session. Caller candidate order never influences the resulting
3376
+ * ranks or tie data; session provenance can (a session that authenticated
3377
+ * with an API key moves a mixed-credential provider out of the OAuth band).
3378
+ */
3379
+ #buildProviderSelectionPolicy(sessionId?: string): ProviderSelectionPolicy {
3380
+ const { catalogProviders, catalogModels } = buildProviderSelectionCatalog(this.#models);
3381
+ const effectiveAuth = new Map<string, EffectiveProviderAuth>();
3382
+ for (const model of this.#models) {
3383
+ const providerKey = model.provider.trim().toLowerCase();
3384
+ if (!providerKey || effectiveAuth.has(providerKey)) {
3255
3385
  continue;
3256
3386
  }
3257
- result.set(normalized, nextRank);
3258
- nextRank += 1;
3387
+ effectiveAuth.set(providerKey, this.#effectiveProviderAuth(model.provider, sessionId));
3259
3388
  }
3260
- for (const model of models) {
3261
- const normalized = model.provider.toLowerCase();
3262
- if (result.has(normalized)) {
3263
- continue;
3389
+ return createProviderSelectionPolicy({
3390
+ explicitProviderOrder: getConfiguredProviderOrderFromSettings(),
3391
+ effectiveAuth,
3392
+ catalogProviders,
3393
+ catalogModels,
3394
+ });
3395
+ }
3396
+
3397
+ #providerRankMap(policy: ProviderSelectionPolicy): Map<string, number> {
3398
+ const providerRank = new Map<string, number>();
3399
+ for (const provider of policy.orderedProviders()) {
3400
+ if (!providerRank.has(provider)) {
3401
+ providerRank.set(provider, policy.rank(provider));
3264
3402
  }
3265
- result.set(normalized, nextRank);
3266
- nextRank += 1;
3267
3403
  }
3268
- return result;
3404
+ return providerRank;
3405
+ }
3406
+
3407
+ /** Stable model order from the registry catalog (never caller candidate order). */
3408
+ #catalogModelOrder(): Map<string, number> {
3409
+ const modelOrder = new Map<string, number>();
3410
+ for (let index = 0; index < this.#models.length; index += 1) {
3411
+ const selector = formatCanonicalVariantSelector(this.#models[index]!);
3412
+ if (!modelOrder.has(selector)) {
3413
+ modelOrder.set(selector, index);
3414
+ }
3415
+ }
3416
+ return modelOrder;
3269
3417
  }
3270
3418
 
3271
3419
  #rememberCanonicalVariant(sessionId: string, selector: string): void {
3272
- this.#sessionCanonicalVariants.delete(sessionId);
3273
- this.#sessionCanonicalVariants.set(sessionId, selector);
3420
+ const normalizedSessionId = sessionId.trim();
3421
+ if (!normalizedSessionId) return;
3422
+ this.#sessionCanonicalVariants.delete(normalizedSessionId);
3423
+ this.#sessionCanonicalVariants.set(normalizedSessionId, selector);
3274
3424
  if (this.#sessionCanonicalVariants.size > MAX_SESSION_CANONICAL_VARIANTS) {
3275
3425
  this.#sessionCanonicalVariants.delete(this.#sessionCanonicalVariants.keys().next().value!);
3276
3426
  }
3277
3427
  }
3428
+
3429
+ /**
3430
+ * Resolve the winning variant among equivalent candidates. Session stickiness
3431
+ * wins when the remembered selector is still eligible; otherwise the variant
3432
+ * is ranked with provider-rank-first axis order when `providerRankFirst` is
3433
+ * set (alias resolution), or the legacy vision-first order by default
3434
+ * (canonical resolution). `exactnessKey` (the alias lookup key) replaces the
3435
+ * canonical-id exactness axis so `model.id === alias` beats slash-prefixed
3436
+ * ids before source/cost/catalog ties. Provider/model tie data always comes
3437
+ * from the registry catalog.
3438
+ */
3278
3439
  #resolveCanonicalVariant(
3279
3440
  variants: readonly CanonicalModelVariant[],
3280
- allCandidates: readonly Model<Api>[],
3281
3441
  sessionId?: string,
3442
+ options: { providerRankFirst?: boolean; exactnessKey?: string; credentialSessionId?: string } = {},
3282
3443
  ): CanonicalModelVariant | undefined {
3283
3444
  if (variants.length === 0) return undefined;
3284
- const stickySelector = sessionId ? this.#sessionCanonicalVariants.get(sessionId) : undefined;
3285
- const stickyVariant = stickySelector ? variants.find(variant => variant.selector === stickySelector) : undefined;
3445
+ const normalizedSessionId = sessionId?.trim();
3446
+ const stickySelector = normalizedSessionId ? this.#sessionCanonicalVariants.get(normalizedSessionId) : undefined;
3447
+ const stickyVariant = stickySelector
3448
+ ? variants.find(variant => variant.selector.toLowerCase() === stickySelector.toLowerCase())
3449
+ : undefined;
3286
3450
  if (stickyVariant) {
3287
- this.#rememberCanonicalVariant(sessionId!, stickyVariant.selector);
3451
+ this.#rememberCanonicalVariant(normalizedSessionId!, stickyVariant.selector);
3288
3452
  return stickyVariant;
3289
3453
  }
3290
- if (sessionId && stickySelector) this.#sessionCanonicalVariants.delete(sessionId);
3291
- const providerRank = this.#providerRank(allCandidates);
3292
- const modelOrder = new Map<string, number>();
3293
- for (let index = 0; index < allCandidates.length; index += 1) {
3294
- modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
3295
- }
3454
+ if (normalizedSessionId && stickySelector) this.#sessionCanonicalVariants.delete(normalizedSessionId);
3455
+ const policy = this.#buildProviderSelectionPolicy(options.credentialSessionId ?? normalizedSessionId);
3456
+ const providerRank = this.#providerRankMap(policy);
3457
+ const modelOrder = this.#catalogModelOrder();
3296
3458
  const sourceRank: Record<CanonicalModelVariant["source"], number> = {
3297
3459
  override: 1,
3298
3460
  bundled: 1,
@@ -3301,8 +3463,10 @@ export class ModelRegistry {
3301
3463
  };
3302
3464
  return [...variants].sort((left, right) =>
3303
3465
  compareEquivalentModelVariants(left.model, right.model, {
3466
+ providerRankFirst: options.providerRankFirst,
3304
3467
  providerRank,
3305
- canonicalId: left.canonicalId === right.canonicalId ? left.canonicalId : undefined,
3468
+ canonicalId:
3469
+ options.exactnessKey ?? (left.canonicalId === right.canonicalId ? left.canonicalId : undefined),
3306
3470
  leftSourceRank: sourceRank[left.source],
3307
3471
  rightSourceRank: sourceRank[right.source],
3308
3472
  includeCost: true,
@@ -3335,15 +3499,142 @@ export class ModelRegistry {
3335
3499
  return this.#filterCanonicalVariants(record, options);
3336
3500
  }
3337
3501
 
3502
+ /**
3503
+ * Resolve an exact canonical id to a concrete model.
3504
+ *
3505
+ * Canonical ids remain exact lookup keys, but their provider variant uses the
3506
+ * same provider-rank-first policy as preset aliases. Availability filtering
3507
+ * remains opt-in through `availableOnly`; final-segment aliases never fall
3508
+ * back implicitly — use {@link resolveModelByLookupAlias} for alias intent.
3509
+ */
3338
3510
  resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
3339
3511
  const variants = this.getCanonicalVariants(canonicalId, options);
3340
3512
  if (variants.length === 0) return undefined;
3341
- const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
3342
- const resolved = this.#resolveCanonicalVariant(variants, candidates, options?.sessionId);
3513
+ const resolved = this.#resolveCanonicalVariant(variants, options?.sessionId, {
3514
+ providerRankFirst: true,
3515
+ credentialSessionId: options?.credentialSessionId,
3516
+ });
3343
3517
  if (resolved && options?.sessionId) this.#rememberCanonicalVariant(options.sessionId, resolved.selector);
3344
3518
  return resolved?.model;
3345
3519
  }
3346
3520
 
3521
+ /**
3522
+ * Resolve a final-slash-segment alias to a concrete model, explicitly.
3523
+ *
3524
+ * The alias gathers every matching variant selector from the variant-level
3525
+ * alias index — never sibling variants in the same canonical record that end
3526
+ * in a different segment — then ranks all eligible variants together by the
3527
+ * centralized provider policy (provider-rank-first axis order), with the
3528
+ * canonical/exactness axis preserved relative to the alias lookup key
3529
+ * (`model.id === alias` beats slash-prefixed ids before source/cost/catalog
3530
+ * ties). Fails closed: availability/disabled filtering applies even to
3531
+ * supplied candidate arrays, alias variants are intersected with the
3532
+ * filtered candidate selectors before ranking, and zero eligible candidates
3533
+ * returns an authoritative `undefined` without rewriting the variant's
3534
+ * model/wire ids. Winners stay sticky per session.
3535
+ */
3536
+ resolveModelByLookupAlias(alias: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
3537
+ const normalizedAlias = alias.trim().toLowerCase();
3538
+ const aliasSelectors = this.#canonicalIndex.aliases.get(normalizedAlias);
3539
+ if (!aliasSelectors || aliasSelectors.length === 0) return undefined;
3540
+
3541
+ const candidateKeys = options?.candidates
3542
+ ? new Set(
3543
+ options.candidates
3544
+ .filter(candidate => this.#isModelAvailable(candidate))
3545
+ .map(candidate => formatCanonicalVariantSelector(candidate)),
3546
+ )
3547
+ : undefined;
3548
+ const eligible: CanonicalModelVariant[] = [];
3549
+ for (const aliasSelector of aliasSelectors) {
3550
+ const canonicalId = this.#canonicalIndex.bySelector.get(aliasSelector);
3551
+ if (canonicalId === undefined) continue;
3552
+ const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
3553
+ if (!record) continue;
3554
+ const variant = record.variants.find(entry => entry.selector.toLowerCase() === aliasSelector);
3555
+ if (!variant) continue;
3556
+ if (candidateKeys && !candidateKeys.has(variant.selector)) continue;
3557
+ if (!this.#isModelAvailable(variant.model)) continue;
3558
+ eligible.push(variant);
3559
+ }
3560
+ if (eligible.length === 0) return undefined;
3561
+
3562
+ const resolved = this.#resolveCanonicalVariant(eligible, options?.sessionId, {
3563
+ providerRankFirst: true,
3564
+ exactnessKey: normalizedAlias,
3565
+ credentialSessionId: options?.credentialSessionId,
3566
+ });
3567
+ if (resolved && options?.sessionId) this.#rememberCanonicalVariant(options.sessionId, resolved.selector);
3568
+ return resolved?.model;
3569
+ }
3570
+
3571
+ /**
3572
+ * Whether a final-slash-segment alias is known in the current canonical
3573
+ * index. Knownness is decided from the full multi-target alias index and is
3574
+ * independent of availability: a known-but-unavailable alias still reports
3575
+ * `true` while {@link resolveModelByLookupAlias} returns an authoritative
3576
+ * `undefined`.
3577
+ */
3578
+ lookupAliasExists(alias: string): boolean {
3579
+ const normalized = alias.trim().toLowerCase();
3580
+ return this.#canonicalIndex.aliases.has(normalized);
3581
+ }
3582
+
3583
+ /**
3584
+ * Effective credential provenance for a provider, derived from existing
3585
+ * AuthStorage/session credential surfaces (never from token shape).
3586
+ * Session-specific provenance wins; API-key surfaces (runtime, config,
3587
+ * custom/manual, stored api_key) beat OAuth presence; OAuth remains the
3588
+ * provenance when it is the effective remaining stored credential;
3589
+ * unknown/keyless providers fall back to non-OAuth.
3590
+ */
3591
+ getEffectiveProviderAuth(provider: string, sessionId?: string): EffectiveProviderAuth {
3592
+ return this.#effectiveProviderAuth(provider, sessionId);
3593
+ }
3594
+
3595
+ /**
3596
+ * Forget a session's remembered canonical variant so the next resolution
3597
+ * for that session re-ranks from scratch (explicit reselection
3598
+ * integration). Returns whether an entry was actually removed.
3599
+ */
3600
+ clearCanonicalVariant(sessionId: string): boolean {
3601
+ const scope = sessionId.trim();
3602
+ if (!scope) return false;
3603
+ return this.#sessionCanonicalVariants.delete(scope);
3604
+ }
3605
+ /**
3606
+ * Snapshot a session's remembered sticky canonical variant selector — the exact
3607
+ * concrete "provider/id" selector, captured verbatim rather than re-derived
3608
+ * from any live model. Returns undefined when the session has no remembered
3609
+ * variant. The caller owns restoring it later via
3610
+ * {@link restoreSessionCanonicalVariant}.
3611
+ */
3612
+ getSessionCanonicalVariant(sessionId: string): string | undefined {
3613
+ const scope = sessionId.trim();
3614
+ if (!scope) return undefined;
3615
+ return this.#sessionCanonicalVariants.get(scope);
3616
+ }
3617
+
3618
+ /**
3619
+ * Restore a session's sticky canonical variant selector exactly, preserving the
3620
+ * concrete provider/model previously remembered (never re-derived from a live
3621
+ * model). The selector must still be present in the canonical index, otherwise
3622
+ * the stale variant is left untouched and `false` is returned.
3623
+ */
3624
+ restoreSessionCanonicalVariant(sessionId: string, selector: string): boolean {
3625
+ const scope = sessionId.trim();
3626
+ if (!scope) return false;
3627
+ const normalized = selector.trim();
3628
+ if (!normalized) return false;
3629
+ const canonicalId = this.#canonicalIndex.bySelector.get(normalized.toLowerCase());
3630
+ if (!canonicalId) return false;
3631
+ const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
3632
+ const variant = record?.variants.find(candidate => candidate.selector.toLowerCase() === normalized.toLowerCase());
3633
+ if (!variant) return false;
3634
+ this.#rememberCanonicalVariant(scope, variant.selector);
3635
+ return true;
3636
+ }
3637
+
3347
3638
  getCanonicalId(model: Model<Api>): string | undefined {
3348
3639
  return this.#canonicalIndex.bySelector.get(formatCanonicalVariantSelector(model).toLowerCase());
3349
3640
  }
@@ -3499,6 +3790,10 @@ export class ModelRegistry {
3499
3790
  return this.#keylessProviders.has(provider) || this.authStorage.hasAuth(provider);
3500
3791
  }
3501
3792
 
3793
+ isCredentiallessProvider(provider: string): boolean {
3794
+ return this.#isCredentiallessProvider(provider);
3795
+ }
3796
+
3502
3797
  getDiscoverableProviders(): string[] {
3503
3798
  const disabledProviders = getDisabledProviderIdsFromSettings();
3504
3799
  return this.#discoveryManager.providers
@@ -3796,12 +4091,15 @@ export class ModelRegistry {
3796
4091
  return this.#applyRuntimeProviderOverride(model, runtimeTransportOverride);
3797
4092
  })
3798
4093
  : nextModels;
4094
+ const withModelOverrides = this.#applyProviderModelOverrides(providerName, withRuntimeTransportOverride);
3799
4095
 
3800
4096
  if (config.oauth?.modifyModels) {
3801
4097
  const credential = this.authStorage.getOAuthCredential(providerName);
3802
4098
  if (credential) {
3803
4099
  this.#models = applyFinalCodexGpt56ContextCap(
3804
- config.oauth.modifyModels(withRuntimeTransportOverride, credential),
4100
+ config.oauth.modifyModels(withModelOverrides, credential),
4101
+ undefined,
4102
+ this.#codexContextWindowOverrides,
3805
4103
  );
3806
4104
  this.#rebuildCanonicalIndex();
3807
4105
  this.#rebuildProviderActivity();
@@ -3809,7 +4107,11 @@ export class ModelRegistry {
3809
4107
  }
3810
4108
  }
3811
4109
 
3812
- this.#models = applyFinalCodexGpt56ContextCap(withRuntimeTransportOverride);
4110
+ this.#models = applyFinalCodexGpt56ContextCap(
4111
+ withModelOverrides,
4112
+ undefined,
4113
+ this.#codexContextWindowOverrides,
4114
+ );
3813
4115
  this.#rebuildCanonicalIndex();
3814
4116
  this.#rebuildProviderActivity();
3815
4117
  return;