@gajae-code/coding-agent 0.15.5 → 0.15.6

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 (155) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  4. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  5. package/dist/types/cli/update-cli.d.ts +2 -0
  6. package/dist/types/commands/auth-gateway.d.ts +3 -0
  7. package/dist/types/commands/gc.d.ts +13 -0
  8. package/dist/types/commands/model-presets.d.ts +21 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +96 -0
  10. package/dist/types/config/file-lock.d.ts +49 -2
  11. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  12. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  13. package/dist/types/config/keybindings.d.ts +3 -2
  14. package/dist/types/config/model-preset-registry.d.ts +368 -0
  15. package/dist/types/config/model-profile-activation.d.ts +39 -0
  16. package/dist/types/config/model-profiles.d.ts +5 -3
  17. package/dist/types/config/model-registry.d.ts +11 -5
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  20. package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  22. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  23. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  25. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/main.d.ts +3 -1
  28. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  29. package/dist/types/modes/jobs-observer.d.ts +36 -0
  30. package/dist/types/sdk/broker/broker.d.ts +3 -0
  31. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  32. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  33. package/dist/types/sdk/client/discovery.d.ts +1 -0
  34. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  35. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  36. package/dist/types/sdk/host/session-runtime.d.ts +15 -1
  37. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  38. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  39. package/dist/types/sdk/router/session-router.d.ts +9 -0
  40. package/dist/types/session/account-inventory.d.ts +1 -0
  41. package/dist/types/session/agent-session.d.ts +19 -7
  42. package/dist/types/session/fold-coordinator.d.ts +201 -0
  43. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  44. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  45. package/dist/types/telemetry/control.d.ts +11 -0
  46. package/dist/types/telemetry/events.d.ts +20 -0
  47. package/dist/types/telemetry/index.d.ts +3 -0
  48. package/dist/types/telemetry/transport.d.ts +22 -0
  49. package/dist/types/tools/bash-interactive.d.ts +23 -0
  50. package/dist/types/tools/index.d.ts +6 -5
  51. package/dist/types/web/search/providers/base.d.ts +9 -4
  52. package/dist/types/web/search/providers/xai.d.ts +2 -1
  53. package/dist/types/web/search/types.d.ts +14 -0
  54. package/package.json +10 -7
  55. package/scripts/generate-hotkeys-docs.ts +4 -0
  56. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  57. package/src/async/job-manager.ts +671 -39
  58. package/src/cli/auth-broker-cli.ts +40 -12
  59. package/src/cli/auth-gateway-cli.ts +273 -97
  60. package/src/cli/config-cli.ts +2 -2
  61. package/src/cli/fast-help.ts +3 -0
  62. package/src/cli/model-presets-cli.ts +100 -0
  63. package/src/cli/update-cli.ts +40 -5
  64. package/src/cli.ts +6 -2
  65. package/src/commands/auth-gateway.ts +12 -8
  66. package/src/commands/gc.ts +19 -0
  67. package/src/commands/model-presets.ts +24 -0
  68. package/src/config/autorouting-tier-map.ts +32 -0
  69. package/src/config/file-lock-gc.ts +9 -4
  70. package/src/config/file-lock.ts +775 -59
  71. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  72. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  73. package/src/config/keybindings.ts +6 -2
  74. package/src/config/model-preset-registry.ts +2799 -0
  75. package/src/config/model-profile-activation.ts +115 -28
  76. package/src/config/model-profile-contract.ts +4 -1
  77. package/src/config/model-profiles.ts +51 -9
  78. package/src/config/model-registry.ts +1021 -122
  79. package/src/config/settings-schema.ts +11 -0
  80. package/src/config/settings.ts +6 -1
  81. package/src/coordinator-mcp/model-preset.ts +32 -17
  82. package/src/coordinator-mcp/projection-scan.ts +571 -0
  83. package/src/coordinator-mcp/server.ts +19 -24
  84. package/src/coordinator-mcp/session-reaper.ts +9 -3
  85. package/src/extensibility/extensions/loader.ts +0 -1
  86. package/src/extensibility/extensions/types.ts +2 -3
  87. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  88. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  89. package/src/gjc-runtime/gc-render.ts +29 -0
  90. package/src/gjc-runtime/gc-runtime.ts +174 -11
  91. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  92. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  93. package/src/index.ts +1 -0
  94. package/src/internal-urls/docs-index.generated.ts +9 -8
  95. package/src/lsp/config.ts +4 -3
  96. package/src/lsp/path-trust.ts +12 -6
  97. package/src/main.ts +9 -2
  98. package/src/modes/acp/acp-agent.ts +11 -5
  99. package/src/modes/components/jobs-overlay-model.ts +99 -16
  100. package/src/modes/components/model-selector.ts +158 -18
  101. package/src/modes/components/status-line/segments.ts +6 -1
  102. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  103. package/src/modes/controllers/input-controller.ts +23 -6
  104. package/src/modes/interactive-mode.ts +14 -1
  105. package/src/modes/jobs-observer.ts +149 -16
  106. package/src/modes/runtime-init.ts +1 -1
  107. package/src/modes/utils/injected-user-submission.ts +15 -1
  108. package/src/prompts/tools/bash.md +2 -2
  109. package/src/prompts/tools/fold-receipt.md +3 -0
  110. package/src/prompts/tools/job.md +1 -1
  111. package/src/sdk/acp/adapter.ts +17 -2
  112. package/src/sdk/broker/broker.ts +48 -21
  113. package/src/sdk/broker/lifecycle.ts +32 -6
  114. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  115. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  116. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  117. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  118. package/src/sdk/bus/slack-conversation.ts +5 -1
  119. package/src/sdk/cli/session-cli.ts +12 -1
  120. package/src/sdk/client/discovery.ts +3 -1
  121. package/src/sdk/host/control/dispatch.ts +3 -0
  122. package/src/sdk/host/model-pin.ts +185 -25
  123. package/src/sdk/host/sdk-run-capability.ts +1 -0
  124. package/src/sdk/host/session-runtime.test.ts +371 -37
  125. package/src/sdk/host/session-runtime.ts +364 -52
  126. package/src/sdk/prompt-deadline-manager.ts +16 -1
  127. package/src/sdk/protocol/adapter-validation.ts +27 -0
  128. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  129. package/src/sdk/protocol/operation-registry.ts +4 -1
  130. package/src/sdk/router/session-router.ts +228 -96
  131. package/src/sdk/session.ts +83 -12
  132. package/src/session/account-inventory.ts +27 -11
  133. package/src/session/agent-session.ts +678 -103
  134. package/src/session/fold-coordinator.ts +433 -0
  135. package/src/session/sdk-run-capability-internal.ts +15 -0
  136. package/src/session/sdk-run-capability.ts +2 -0
  137. package/src/session/session-manager.ts +5 -4
  138. package/src/setup/provider-presets.json +1 -1
  139. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  140. package/src/task/executor.ts +19 -3
  141. package/src/telemetry/control.ts +26 -0
  142. package/src/telemetry/events.ts +142 -0
  143. package/src/telemetry/index.ts +3 -0
  144. package/src/telemetry/transport.ts +87 -0
  145. package/src/tools/bash-interactive.ts +182 -85
  146. package/src/tools/bash.ts +772 -67
  147. package/src/tools/index.ts +7 -5
  148. package/src/tools/tool-catalog.generated.ts +1 -1
  149. package/src/web/search/provider.ts +120 -11
  150. package/src/web/search/providers/anthropic.ts +89 -17
  151. package/src/web/search/providers/base.ts +9 -4
  152. package/src/web/search/providers/gemini.ts +87 -15
  153. package/src/web/search/providers/openai-compatible.ts +23 -8
  154. package/src/web/search/providers/xai.ts +98 -12
  155. package/src/web/search/types.ts +16 -0
@@ -6,7 +6,9 @@ import { clampExplicitThinkingLevelForModel, formatClampedModelSelector } from "
6
6
  import { validateModelProfileName } from "./model-profile-contract";
7
7
  import {
8
8
  aggregateModelProfileRequiredProviders,
9
+ deriveModelProfileMappedProviders,
9
10
  formatModelProfileDisplayLabel,
11
+ type ModelProfileDefinition,
10
12
  PROXY_ROUTABLE_PROVIDER_IDS,
11
13
  resolveProfileBindings,
12
14
  } from "./model-profiles";
@@ -537,19 +539,40 @@ function rewriteBindingsProviders(
537
539
  * preset. Returns undefined when unset or empty. Passwords/labels are never
538
540
  * treated as proxy ids here; only lowercase provider ids from settings.
539
541
  */
540
- function resolveProxyProviderId(settings: Pick<Settings, "get" | "getGlobal" | "getOverride">): string | undefined {
541
- const value = settings.get("modelProfile.proxyProvider");
542
- if (typeof value !== "string" || value.trim() === "") return undefined;
543
- const id = value.trim().toLowerCase();
544
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) {
542
+ export function resolveProxyProviderId(settings: Pick<Settings, "get"> | undefined): string | undefined {
543
+ const config = inspectProxyProviderId(settings);
544
+ if (config.status === "unset") return undefined;
545
+ if (config.status === "invalid") {
545
546
  throw new Error(
546
- `modelProfile.proxyProvider must be a lowercase provider id (got "${value.trim()}"). Configure an OpenAI-compatible proxy with \`gjc setup provider\`, then set its id here.`,
547
+ `modelProfile.proxyProvider must be a lowercase provider id (got "${config.value}"). Configure an OpenAI-compatible proxy with \`gjc setup provider\`, then set its id here.`,
547
548
  );
548
549
  }
549
- return id;
550
+ return config.id;
551
+ }
552
+
553
+ export type ProxyProviderConfig =
554
+ | { status: "unset" }
555
+ | { status: "configured"; id: string }
556
+ | { status: "invalid"; value: string };
557
+
558
+ export function inspectProxyProviderId(settings: Pick<Settings, "get"> | undefined): ProxyProviderConfig {
559
+ if (!settings) return { status: "unset" };
560
+ const value = settings.get("modelProfile.proxyProvider");
561
+ if (typeof value !== "string" || value.trim() === "") return { status: "unset" };
562
+ const id = value.trim().toLowerCase();
563
+ return /^[a-z0-9][a-z0-9._-]*$/.test(id) ? { status: "configured", id } : { status: "invalid", value: value.trim() };
564
+ }
565
+
566
+ /** Passive surfaces fail closed on malformed settings instead of throwing. */
567
+ export function tryResolveProxyProviderId(settings: Pick<Settings, "get"> | undefined): string | undefined {
568
+ const config = inspectProxyProviderId(settings);
569
+ return config.status === "configured" ? config.id : undefined;
550
570
  }
551
571
 
552
- function resolveProxyMode(settings: Pick<Settings, "get" | "getGlobal" | "getOverride">): "fallback" | "always" {
572
+ export type ModelProfileProxyMode = "fallback" | "always";
573
+
574
+ export function resolveProxyMode(settings: Pick<Settings, "get"> | undefined): ModelProfileProxyMode {
575
+ if (!settings) return "fallback";
553
576
  const value = settings.get("modelProfile.proxyMode");
554
577
  if (value === undefined || value === "fallback" || value === "always") return value ?? "fallback";
555
578
  throw new Error(`modelProfile.proxyMode must be "fallback" or "always" (got "${String(value)}")`);
@@ -563,12 +586,35 @@ function resolveProxyMode(settings: Pick<Settings, "get" | "getGlobal" | "getOve
563
586
  * not retain the upstream provider prefix. Missing or ambiguous matches fail
564
587
  * closed rather than leaving an unauthenticated role selector behind.
565
588
  */
566
- function rewriteSelectorForProxy(
589
+ export function getProxyRoutableProviders(profile: ModelProfileDefinition): ReadonlySet<string> {
590
+ if (profile.source === "user") return new Set();
591
+ return profile.source === "registry"
592
+ ? new Set([
593
+ ...PROXY_ROUTABLE_PROVIDER_IDS,
594
+ ...profile.requiredProviders,
595
+ ...deriveModelProfileMappedProviders(profile),
596
+ ])
597
+ : PROXY_ROUTABLE_PROVIDER_IDS;
598
+ }
599
+
600
+ /**
601
+ * Profile sources whose non-default qualified role bindings are part of the
602
+ * activation contract. Proxy rewrites run before role resolution, so a
603
+ * rewritten selector satisfies this prerequisite when its concrete proxy
604
+ * model is present in the effective catalog. Embedded built-ins intentionally
605
+ * retain their legacy tolerance for stale qualified role tails.
606
+ */
607
+ export function requiresQualifiedModelProfileRoleResolution(profile: Pick<ModelProfileDefinition, "source">): boolean {
608
+ return profile.source === "user" || profile.source === "registry";
609
+ }
610
+
611
+ export function rewriteSelectorForProxy(
567
612
  selector: string,
568
613
  proxyProvider: string,
569
- proxyMode: "fallback" | "always",
614
+ proxyMode: ModelProfileProxyMode,
570
615
  allModels: Model<Api>[],
571
616
  directlyAuthenticated: ReadonlySet<string>,
617
+ routableProviders: ReadonlySet<string>,
572
618
  ): string {
573
619
  const suffix = splitSelectorThinkingSuffix(selector);
574
620
  const baseSelector = suffix.selector;
@@ -589,7 +635,7 @@ function rewriteSelectorForProxy(
589
635
  }
590
636
  const directProvider = baseSelector.substring(0, slash);
591
637
  if (proxyMode === "fallback" && directlyAuthenticated.has(directProvider)) return selector;
592
- if (!PROXY_ROUTABLE_PROVIDER_IDS.has(directProvider)) return selector;
638
+ if (!routableProviders.has(directProvider)) return selector;
593
639
  if (proxyProvider === directProvider) {
594
640
  throw new Error(`Configured proxy "${proxyProvider}" cannot route its own direct selector "${baseSelector}"`);
595
641
  }
@@ -613,9 +659,10 @@ function rewriteSelectorValueForProxy(
613
659
  proxyMode: "fallback" | "always",
614
660
  allModels: Model<Api>[],
615
661
  directlyAuthenticated: ReadonlySet<string>,
662
+ routableProviders: ReadonlySet<string>,
616
663
  ): ModelSelectorValue {
617
664
  const selectors = normalizeModelSelectorValue(selectorValue).map(selector =>
618
- rewriteSelectorForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
665
+ rewriteSelectorForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated, routableProviders),
619
666
  );
620
667
  return selectors.length === 1 && typeof selectorValue === "string" ? selectors[0] : selectors;
621
668
  }
@@ -630,6 +677,7 @@ function rewriteBindingsForProxy(
630
677
  proxyMode: "fallback" | "always",
631
678
  allModels: Model<Api>[],
632
679
  directlyAuthenticated: ReadonlySet<string>,
680
+ routableProviders: ReadonlySet<string>,
633
681
  ): {
634
682
  defaultSelector?: ModelSelectorValue;
635
683
  modelRoles: Record<string, ModelSelectorValue>;
@@ -643,18 +691,33 @@ function rewriteBindingsForProxy(
643
691
  proxyMode,
644
692
  allModels,
645
693
  directlyAuthenticated,
694
+ routableProviders,
646
695
  )
647
696
  : undefined,
648
697
  modelRoles: Object.fromEntries(
649
698
  Object.entries(bindings.modelRoles).map(([role, selector]) => [
650
699
  role,
651
- rewriteSelectorValueForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
700
+ rewriteSelectorValueForProxy(
701
+ selector,
702
+ proxyProvider,
703
+ proxyMode,
704
+ allModels,
705
+ directlyAuthenticated,
706
+ routableProviders,
707
+ ),
652
708
  ]),
653
709
  ),
654
710
  agentModelOverrides: Object.fromEntries(
655
711
  Object.entries(bindings.agentModelOverrides).map(([role, selector]) => [
656
712
  role,
657
- rewriteSelectorValueForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
713
+ rewriteSelectorValueForProxy(
714
+ selector,
715
+ proxyProvider,
716
+ proxyMode,
717
+ allModels,
718
+ directlyAuthenticated,
719
+ routableProviders,
720
+ ),
658
721
  ]),
659
722
  ),
660
723
  };
@@ -888,13 +951,25 @@ export async function prepareModelProfileActivation(
888
951
  const requiredProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
889
952
  const alternativeGroups = profile.alternativeProviderGroups ?? [];
890
953
  const alternativeSet = new Set(alternativeGroups.flat());
954
+ const requiredProviderSet = new Set(requiredProviders);
955
+ const authenticationProbeProviders = new Set<string>([
956
+ ...requiredProviders,
957
+ ...alternativeSet,
958
+ ...deriveModelProfileMappedProviders(profile),
959
+ ]);
891
960
 
892
961
  const missingProviders: string[] = [];
893
962
  const authenticatedProviders: string[] = [];
894
- for (const provider of requiredProviders) {
895
- const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, credentialSessionId);
963
+ for (const provider of authenticationProbeProviders) {
964
+ let apiKey: string | undefined;
965
+ try {
966
+ apiKey = await options.modelRegistry.getApiKeyForProvider(provider, credentialSessionId);
967
+ } catch (error) {
968
+ if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
969
+ continue;
970
+ }
896
971
  if (apiKey !== kNoAuth && !isAuthenticated(apiKey)) {
897
- missingProviders.push(provider);
972
+ if (requiredProviderSet.has(provider)) missingProviders.push(provider);
898
973
  } else {
899
974
  authenticatedProviders.push(provider);
900
975
  }
@@ -905,8 +980,18 @@ export async function prepareModelProfileActivation(
905
980
  // A proxy-routable strict provider is satisfied through the configured
906
981
  // OpenAI-compatible proxy when that proxy is itself authenticated; otherwise
907
982
  // we fail closed pointing at the proxy (or the provider when none is set).
908
- const proxyProvider = profile.source === "builtin" ? resolveProxyProviderId(options.settings) : undefined;
909
- const proxyMode = profile.source === "builtin" ? resolveProxyMode(options.settings) : "fallback";
983
+ const proxyProvider = profile.source !== "user" ? resolveProxyProviderId(options.settings) : undefined;
984
+ const proxyMode = profile.source !== "user" ? resolveProxyMode(options.settings) : "fallback";
985
+ const proxyRoutableProviders =
986
+ profile.source === "user"
987
+ ? new Set<string>()
988
+ : profile.source === "registry"
989
+ ? new Set([
990
+ ...PROXY_ROUTABLE_PROVIDER_IDS,
991
+ ...profile.requiredProviders,
992
+ ...deriveModelProfileMappedProviders(profile),
993
+ ])
994
+ : PROXY_ROUTABLE_PROVIDER_IDS;
910
995
  if (proxyMode === "always" && proxyProvider === undefined) {
911
996
  throw new Error('modelProfile.proxyMode "always" requires modelProfile.proxyProvider');
912
997
  }
@@ -931,13 +1016,13 @@ export async function prepareModelProfileActivation(
931
1016
  }
932
1017
 
933
1018
  const strictMissing = missingProviders.filter(
934
- provider => !PROXY_ROUTABLE_PROVIDER_IDS.has(provider) && !alternativeSet.has(provider),
1019
+ provider => !proxyRoutableProviders.has(provider) && !alternativeSet.has(provider),
935
1020
  );
936
1021
  if (strictMissing.length > 0) {
937
1022
  throw new ModelProfileCredentialError(profileLabel, strictMissing);
938
1023
  }
939
1024
  const strictRoutableMissing = missingProviders.filter(
940
- provider => PROXY_ROUTABLE_PROVIDER_IDS.has(provider) && !alternativeSet.has(provider),
1025
+ provider => proxyRoutableProviders.has(provider) && !alternativeSet.has(provider),
941
1026
  );
942
1027
  if (strictRoutableMissing.length > 0 && (proxyProvider === undefined || !proxyAuthenticated)) {
943
1028
  throw new ModelProfileCredentialError(
@@ -948,7 +1033,7 @@ export async function prepareModelProfileActivation(
948
1033
  for (const group of alternativeGroups) {
949
1034
  const groupAuthenticated = group.some(provider => authenticatedProviders.includes(provider));
950
1035
  if (groupAuthenticated) continue;
951
- const allRoutable = group.every(provider => PROXY_ROUTABLE_PROVIDER_IDS.has(provider));
1036
+ const allRoutable = group.every(provider => proxyRoutableProviders.has(provider));
952
1037
  if (allRoutable && proxyAuthenticated) continue;
953
1038
  throw new ModelProfileCredentialError(
954
1039
  profileLabel,
@@ -960,20 +1045,22 @@ export async function prepareModelProfileActivation(
960
1045
  options.modelRegistry.getAvailableForProfileActivation?.() ??
961
1046
  options.modelRegistry.getAvailable?.() ??
962
1047
  options.modelRegistry.getAll();
1048
+ const roleCatalogModels = options.modelRegistry.getAll();
963
1049
  let bindings = resolveProfileBindings(profile);
964
- if (missingProviders.length > 0 && alternativeGroups.length > 0) {
1050
+ if (alternativeGroups.length > 0) {
965
1051
  bindings = rewriteBindingsProviders(bindings, new Set(authenticatedProviders), alternativeGroups);
966
1052
  }
967
1053
  // Built-in preset selectors are routed through a configured authenticated
968
1054
  // proxy according to the selected mode. This session-scoped rewrite is never
969
1055
  // persisted to models.yml.
970
- if (proxyProvider !== undefined && proxyAuthenticated && profile.source === "builtin") {
1056
+ if (proxyProvider !== undefined && proxyAuthenticated && profile.source !== "user") {
971
1057
  bindings = rewriteBindingsForProxy(
972
1058
  bindings,
973
1059
  proxyProvider,
974
1060
  proxyMode,
975
1061
  availableModels,
976
1062
  new Set(authenticatedProviders),
1063
+ proxyRoutableProviders,
977
1064
  );
978
1065
  }
979
1066
  const defaultSelectors = bindings.defaultSelector ? normalizeModelSelectorValue(bindings.defaultSelector) : [];
@@ -1038,14 +1125,14 @@ export async function prepareModelProfileActivation(
1038
1125
  ][]) {
1039
1126
  modelRoles[role] = await resolveAndClampSelectorValue(
1040
1127
  selectorValue,
1041
- availableModels,
1128
+ roleCatalogModels,
1042
1129
  {
1043
1130
  settings: options.settings as Settings,
1044
1131
  modelRegistry: options.modelRegistry as ModelRegistry,
1045
1132
  sessionId: options.session.sessionId,
1046
1133
  credentialSessionId,
1047
1134
  aliasIntent: "preset-equivalent",
1048
- requireQualifiedResolution: profile.source !== "builtin",
1135
+ requireQualifiedResolution: requiresQualifiedModelProfileRoleResolution(profile),
1049
1136
  },
1050
1137
  profileLabel,
1051
1138
  role,
@@ -1059,14 +1146,14 @@ export async function prepareModelProfileActivation(
1059
1146
  ][]) {
1060
1147
  agentModelOverrides[role] = await resolveAndClampSelectorValue(
1061
1148
  selectorValue,
1062
- availableModels,
1149
+ roleCatalogModels,
1063
1150
  {
1064
1151
  settings: options.settings as Settings,
1065
1152
  modelRegistry: options.modelRegistry as ModelRegistry,
1066
1153
  sessionId: options.session.sessionId,
1067
1154
  credentialSessionId,
1068
1155
  aliasIntent: "preset-equivalent",
1069
- requireQualifiedResolution: profile.source !== "builtin",
1156
+ requireQualifiedResolution: requiresQualifiedModelProfileRoleResolution(profile),
1070
1157
  },
1071
1158
  profileLabel,
1072
1159
  role,
@@ -120,7 +120,10 @@ export function projectModelProfileCatalog(
120
120
  .map(([id, definition]) => ({
121
121
  id,
122
122
  displayName: formatModelProfileDisplayLabel(definition),
123
- source: definition.source === "user" ? ("configured" as const) : ("builtin" as const),
123
+ source:
124
+ definition.source === "user" || definition.source === "registry"
125
+ ? ("configured" as const)
126
+ : ("builtin" as const),
124
127
  }))
125
128
  .sort((left, right) => left.id.localeCompare(right.id));
126
129
  }
@@ -8,6 +8,7 @@ export interface ModelProfileDefinition {
8
8
  name: string;
9
9
  requiredProviders: string[];
10
10
  displayName?: string;
11
+ providerGroup?: string;
11
12
  /**
12
13
  * Optional groups of providers that are interchangeable fallbacks.
13
14
  * Each group is an array of provider ids where at least one must be
@@ -19,7 +20,7 @@ export interface ModelProfileDefinition {
19
20
  */
20
21
  alternativeProviderGroups?: readonly (readonly string[])[];
21
22
  modelMapping: Partial<Record<ModelProfileRole, ModelSelectorValue>>;
22
- source: "builtin" | "user";
23
+ source: "builtin" | "registry" | "user";
23
24
  }
24
25
 
25
26
  export interface ResolvedProfileBinding {
@@ -673,12 +674,14 @@ const PROFILE_RECOMMENDATIONS: Record<string, string> = {
673
674
  };
674
675
 
675
676
  export function getModelProfilePresentation(
676
- profile: string | Pick<ModelProfileDefinition, "name" | "displayName">,
677
+ profile: string | Pick<ModelProfileDefinition, "name" | "displayName" | "providerGroup">,
677
678
  ): ModelProfilePresentation {
678
679
  const name = typeof profile === "string" ? profile : profile.name;
679
680
  const displayName = typeof profile === "string" ? undefined : profile.displayName;
680
681
  const presentation = PROFILE_PRESENTATION[name];
681
682
  if (presentation) return presentation;
683
+ if (typeof profile !== "string" && profile.providerGroup)
684
+ return { displayName: formatModelProfileDisplayLabel(profile), providerGroup: profile.providerGroup };
682
685
  return { displayName: formatModelProfileDisplayLabel({ name, displayName }), providerGroup: "CUSTOM" };
683
686
  }
684
687
 
@@ -732,17 +735,56 @@ export function recommendModelProfileForProvider(
732
735
  return recommended ? profiles.get(recommended) : undefined;
733
736
  }
734
737
 
735
- export function mergeModelProfiles(userProfiles?: ModelsConfig["profiles"]): Map<string, ModelProfileDefinition> {
738
+ export type RegistryModelProfilesInput =
739
+ | ReadonlyMap<string, ModelProfileDefinition>
740
+ | readonly ModelProfileDefinition[]
741
+ | Readonly<Record<string, ModelProfileDefinition>>;
742
+
743
+ function cloneModelSelectorValue(value: ModelSelectorValue): ModelSelectorValue {
744
+ return Array.isArray(value) ? [...value] : value;
745
+ }
746
+
747
+ function cloneModelProfileDefinition(
748
+ definition: ModelProfileDefinition,
749
+ source: ModelProfileDefinition["source"] = definition.source,
750
+ ): ModelProfileDefinition {
751
+ return {
752
+ name: definition.name,
753
+ displayName: definition.displayName,
754
+ providerGroup: definition.providerGroup,
755
+ requiredProviders: [...definition.requiredProviders],
756
+ alternativeProviderGroups: definition.alternativeProviderGroups?.map(group => [...group]),
757
+ modelMapping: Object.fromEntries(
758
+ Object.entries(definition.modelMapping).map(([role, value]) => [role, cloneModelSelectorValue(value)]),
759
+ ) as Partial<Record<ModelProfileRole, ModelSelectorValue>>,
760
+ source,
761
+ };
762
+ }
763
+
764
+ function registryProfileEntries(
765
+ input: RegistryModelProfilesInput | undefined,
766
+ ): Array<[string, ModelProfileDefinition]> {
767
+ if (!input) return [];
768
+ if (input instanceof Map) return [...input.entries()];
769
+ if (Array.isArray(input)) return input.map(definition => [definition.name, definition]);
770
+ return Object.entries(input);
771
+ }
772
+
773
+ export function mergeModelProfiles(
774
+ userProfiles?: ModelsConfig["profiles"],
775
+ registryProfiles?: RegistryModelProfilesInput,
776
+ ): Map<string, ModelProfileDefinition> {
736
777
  const profiles = new Map<string, ModelProfileDefinition>();
737
778
  for (const definition of BUILTIN_MODEL_PROFILES) {
738
- profiles.set(definition.name, {
739
- ...definition,
740
- requiredProviders: [...definition.requiredProviders],
741
- modelMapping: { ...definition.modelMapping },
742
- });
779
+ profiles.set(definition.name, cloneModelProfileDefinition(definition, "builtin"));
780
+ }
781
+ for (const [name, definition] of registryProfileEntries(registryProfiles)) {
782
+ profiles.set(name, cloneModelProfileDefinition({ ...definition, name }, "registry"));
743
783
  }
744
784
  for (const [name, definition] of Object.entries(userProfiles ?? {})) {
745
- const modelMapping = { ...definition.model_mapping };
785
+ const modelMapping = Object.fromEntries(
786
+ Object.entries(definition.model_mapping).map(([role, value]) => [role, cloneModelSelectorValue(value)]),
787
+ ) as Partial<Record<ModelProfileRole, ModelSelectorValue>>;
746
788
  profiles.set(name, {
747
789
  name,
748
790
  displayName: definition.display_name,