@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
@@ -15,7 +15,7 @@ import { logger } from "@gajae-code/utils";
15
15
  import chalk from "chalk";
16
16
  import { parseThinkingLevel, resolveThinkingLevelForModel, splitSelectorThinkingSuffix } from "../thinking";
17
17
  import { isAuthenticatedOrKeyless } from "./model-auth";
18
- import { compareEquivalentModelVariants } from "./model-equivalence";
18
+ import { compareEquivalentModelVariants, getFinalSlashSegmentAliasKey } from "./model-equivalence";
19
19
  import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
20
20
  import { type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
21
21
  import type { Settings } from "./settings";
@@ -210,7 +210,16 @@ export interface ModelMatchPreferences {
210
210
  }
211
211
 
212
212
  export type CanonicalModelRegistry = Partial<
213
- Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId" | "seedCanonicalVariant">
213
+ Pick<
214
+ ModelRegistry,
215
+ | "resolveCanonicalModel"
216
+ | "getCanonicalVariants"
217
+ | "getCanonicalId"
218
+ | "seedCanonicalVariant"
219
+ | "resolveModelByLookupAlias"
220
+ | "lookupAliasExists"
221
+ | "clearCanonicalVariant"
222
+ >
214
223
  >;
215
224
  export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable"> & Partial<CanonicalModelRegistry>;
216
225
  type CliModelRegistry = Pick<ModelRegistry, "getAll"> & Partial<CanonicalModelRegistry>;
@@ -362,6 +371,7 @@ function findExactCanonicalModelMatch(
362
371
  availableModels: Model<Api>[],
363
372
  modelRegistry: CanonicalModelRegistry | undefined,
364
373
  sessionId?: string,
374
+ credentialSessionId?: string,
365
375
  ): Model<Api> | undefined {
366
376
  if (!modelRegistry) {
367
377
  return undefined;
@@ -374,6 +384,7 @@ function findExactCanonicalModelMatch(
374
384
  availableOnly: false,
375
385
  candidates: availableModels,
376
386
  sessionId,
387
+ credentialSessionId,
377
388
  });
378
389
  }
379
390
 
@@ -386,6 +397,7 @@ function findExactEquivalentModelMatch(
386
397
  availableModels: Model<Api>[],
387
398
  modelRegistry: CanonicalModelRegistry | undefined,
388
399
  sessionId?: string,
400
+ credentialSessionId?: string,
389
401
  ): Model<Api> | undefined {
390
402
  if (!modelRegistry?.getCanonicalId || !modelRegistry.resolveCanonicalModel) return undefined;
391
403
  const trimmedReference = modelReference.trim();
@@ -402,18 +414,26 @@ function findExactEquivalentModelMatch(
402
414
  availableOnly: false,
403
415
  candidates: availableModels,
404
416
  sessionId,
417
+ credentialSessionId,
405
418
  });
406
419
  }
407
420
 
408
421
  /**
409
- * Try to match a pattern to a model from the available models list.
410
- * Returns the matched model or undefined if no match found.
422
+ * Match a pattern through the strict-exact stages (provider-qualified,
423
+ * canonical, equivalent, exact-ID, and provider-scoped) only. This deliberately
424
+ * excludes substring/alias fuzzy matching so preset-alias resolution can run
425
+ * between strict-exact and fuzzy stages.
411
426
  */
412
- function tryMatchModel(
427
+ function findStrictExactModelMatch(
413
428
  modelPattern: string,
414
429
  availableModels: Model<Api>[],
415
430
  context: ModelPreferenceContext,
416
- options?: { modelRegistry?: CanonicalModelRegistry; sessionId?: string },
431
+ options?: {
432
+ modelRegistry?: CanonicalModelRegistry;
433
+ sessionId?: string;
434
+ credentialSessionId?: string;
435
+ aliasIntent?: "preset-equivalent" | "reject";
436
+ },
417
437
  ): Model<Api> | undefined {
418
438
  // Explicit provider/model selectors always bypass canonical coalescing.
419
439
  const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
@@ -427,6 +447,7 @@ function tryMatchModel(
427
447
  availableModels,
428
448
  options?.modelRegistry,
429
449
  options?.sessionId,
450
+ options?.credentialSessionId,
430
451
  );
431
452
  if (exactCanonicalMatch) {
432
453
  return exactCanonicalMatch;
@@ -437,22 +458,36 @@ function tryMatchModel(
437
458
  availableModels,
438
459
  options?.modelRegistry,
439
460
  options?.sessionId,
461
+ options?.credentialSessionId,
440
462
  );
441
463
  if (exactEquivalentMatch) {
442
464
  return exactEquivalentMatch;
443
465
  }
444
466
 
445
467
  // Exact ID match (case-insensitive) — this must happen before provider-scoped
446
- // fuzzy matching so raw IDs that contain slashes (for example OpenRouter model
468
+ // matching so raw IDs that contain slashes (for example OpenRouter route-suffix
447
469
  // IDs like "openai/gpt-4o:extended") still resolve as IDs instead of being
448
- // misread as a provider-qualified selector.
449
- const exactMatches = availableModels.filter(m => m.id.toLowerCase() === modelPattern.toLowerCase());
450
- if (exactMatches.length > 0) {
451
- return pickPreferredModel(exactMatches, context);
470
+ // misread as a provider-qualified selector. Cross-provider exact-ID lookup is
471
+ // confined to bare IDs and route-suffix IDs: a plain provider pin (provider/id
472
+ // with no route suffix, e.g. "openai/gpt-5") must never cross to a different
473
+ // provider, even when a slash-embedded catalog id collides with the pin's text.
474
+ const slashIndex = modelPattern.indexOf("/");
475
+ const thinkingSuffix = splitSelectorThinkingSuffix(modelPattern);
476
+ const providerSegment = slashIndex === -1 ? "" : modelPattern.slice(0, slashIndex);
477
+ const modelIdSegment = slashIndex === -1 ? modelPattern : modelPattern.slice(slashIndex + 1);
478
+ const isRouteSuffixId =
479
+ slashIndex !== -1 &&
480
+ !providerSegment.includes(":") &&
481
+ modelIdSegment.includes(":") &&
482
+ thinkingSuffix.thinkingLevel === undefined;
483
+ if (slashIndex === -1 || isRouteSuffixId) {
484
+ const exactMatches = availableModels.filter(m => m.id.toLowerCase() === modelPattern.toLowerCase());
485
+ if (exactMatches.length > 0) {
486
+ return pickPreferredModel(exactMatches, context);
487
+ }
452
488
  }
453
489
  // Provider-qualified selectors are exact-only. Case-only duplicate catalog
454
490
  // entries still rank deterministically within the explicitly named provider.
455
- const slashIndex = modelPattern.indexOf("/");
456
491
  if (slashIndex !== -1) {
457
492
  const provider = modelPattern.slice(0, slashIndex).trim().toLowerCase();
458
493
  const modelId = modelPattern
@@ -465,7 +500,23 @@ function tryMatchModel(
465
500
  return providerExactMatches.length > 0 ? pickPreferredModel(providerExactMatches, context) : undefined;
466
501
  }
467
502
 
468
- // No exact match - fall back to partial matching
503
+ return undefined;
504
+ }
505
+
506
+ /**
507
+ * Substring/alias fuzzy matching, run only after strict-exact and preset-alias
508
+ * resolution have both failed so a preset alias never loses to a coincidental
509
+ * substring match.
510
+ */
511
+ function findFuzzyModelMatch(
512
+ modelPattern: string,
513
+ availableModels: Model<Api>[],
514
+ context: ModelPreferenceContext,
515
+ ): Model<Api> | undefined {
516
+ // Provider-qualified selectors are exact pins: they never enter substring/name
517
+ // fuzzy matching, even when a slash-embedded catalog id shares the pin's text.
518
+ // Explicit glob patterns are handled separately by findGlobMatch.
519
+ if (modelPattern.includes("/")) return undefined;
469
520
  const matches = availableModels.filter(
470
521
  m =>
471
522
  m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
@@ -496,6 +547,27 @@ function tryMatchModel(
496
547
  return pickPreferredModel(topCandidates, context);
497
548
  }
498
549
 
550
+ /**
551
+ * Match a pattern against the available models using the full staged policy:
552
+ * strict-exact first, then substring/alias fuzzy matching.
553
+ */
554
+ function tryMatchModel(
555
+ modelPattern: string,
556
+ availableModels: Model<Api>[],
557
+ context: ModelPreferenceContext,
558
+ options?: {
559
+ modelRegistry?: CanonicalModelRegistry;
560
+ sessionId?: string;
561
+ credentialSessionId?: string;
562
+ aliasIntent?: "preset-equivalent" | "reject";
563
+ },
564
+ ): Model<Api> | undefined {
565
+ return (
566
+ findStrictExactModelMatch(modelPattern, availableModels, context, options) ??
567
+ findFuzzyModelMatch(modelPattern, availableModels, context)
568
+ );
569
+ }
570
+
499
571
  export interface ParsedModelResult {
500
572
  model: Model<Api> | undefined;
501
573
  /** Thinking level if explicitly specified in pattern, undefined otherwise */
@@ -509,12 +581,16 @@ export interface ResolveSelectorOptions {
509
581
  modelRegistry?: CanonicalModelRegistry;
510
582
  preferences?: ModelMatchPreferences;
511
583
  sessionId?: string;
584
+ credentialSessionId?: string;
585
+ aliasIntent?: "preset-equivalent" | "reject";
512
586
  }
513
587
 
514
588
  /**
515
589
  * Resolve one selector through ordered exact, canonical, bare-id, provider-fuzzy,
516
- * substring/alias, and glob stages. Thinking is split only after a full selector
517
- * cannot resolve, which preserves OpenRouter route suffixes in concrete IDs.
590
+ * substring/alias, and glob stages. A preset-equivalent alias intent adds a
591
+ * final-slash-segment alias stage between strict-exact and substring/fuzzy.
592
+ * Thinking is split only after a full selector cannot resolve, which preserves
593
+ * OpenRouter route suffixes in concrete IDs.
518
594
  */
519
595
  export function resolveSelector(
520
596
  selector: string,
@@ -522,10 +598,38 @@ export function resolveSelector(
522
598
  options?: ResolveSelectorOptions,
523
599
  ): ParsedModelResult {
524
600
  const context = buildPreferenceContext(candidates, options?.preferences);
525
- const exact = tryMatchModel(selector, candidates, context, options);
601
+ const exact = findStrictExactModelMatch(selector, candidates, context, options);
526
602
  if (exact) return { model: exact, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
603
+
604
+ // Preset-equivalent intent resolves a final-slash-segment alias only after
605
+ // strict exact full-selector/canonical/exact-ID resolution fails and BEFORE
606
+ // substring/fuzzy matching. A known alias that is unavailable in the caller's
607
+ // candidate set fails closed rather than falling through to glob/substring.
608
+ if (options?.aliasIntent === "preset-equivalent") {
609
+ const alias = resolvePresetEquivalentAlias(selector, candidates, options);
610
+ if (alias.model) {
611
+ return {
612
+ model: alias.model,
613
+ thinkingLevel: alias.thinkingLevel,
614
+ warning: alias.warning,
615
+ explicitThinkingLevel: alias.explicitThinkingLevel,
616
+ };
617
+ }
618
+ if (alias.knownButUnavailable) {
619
+ return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
620
+ }
621
+ }
622
+
623
+ const fuzzy = findFuzzyModelMatch(selector, candidates, context);
624
+ if (fuzzy) return { model: fuzzy, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
625
+
527
626
  const glob = findGlobMatch(selector, candidates, context);
528
627
  if (glob) return { model: glob, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
628
+ const selectorSlashIndex = selector.indexOf("/");
629
+ const selectorColonIndex = selector.lastIndexOf(":");
630
+ if (selectorSlashIndex !== -1 && selectorColonIndex !== -1 && selectorColonIndex < selectorSlashIndex) {
631
+ return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
632
+ }
529
633
 
530
634
  const suffix = splitSelectorThinkingSuffix(selector);
531
635
  if (
@@ -535,7 +639,12 @@ export function resolveSelector(
535
639
  return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
536
640
  }
537
641
 
642
+ const suffixExact = findStrictExactModelMatch(suffix.selector, candidates, context, options);
643
+ if (options?.aliasIntent === "preset-equivalent" && !suffix.selector.includes("/") && !suffixExact) {
644
+ return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
645
+ }
538
646
  const model =
647
+ suffixExact ??
539
648
  tryMatchModel(suffix.selector, candidates, context, options) ??
540
649
  findGlobMatch(suffix.selector, candidates, context);
541
650
  if (!model) return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
@@ -550,6 +659,84 @@ export function resolveSelector(
550
659
  };
551
660
  }
552
661
 
662
+ interface PresetEquivalentAliasResolution {
663
+ model?: Model<Api>;
664
+ thinkingLevel?: ThinkingLevel;
665
+ explicitThinkingLevel: boolean;
666
+ warning?: string;
667
+ knownButUnavailable?: boolean;
668
+ }
669
+
670
+ /**
671
+ * Resolve an unqualified preset assignment through the canonical registry's
672
+ * final-segment alias lookup. Preserves exact full-selector/canonical/exact-ID
673
+ * precedence: it is only consulted after exact resolution fails. A selector
674
+ * containing `/` is an explicit provider pin and never aliases. A known-but-
675
+ * unavailable bare alias fails closed (never falls through to glob). Thinking
676
+ * suffixes are split once so `alias:high` keeps its thinking level.
677
+ */
678
+ function resolvePresetEquivalentAlias(
679
+ selector: string,
680
+ availableModels: Model<Api>[],
681
+ options: Pick<
682
+ ResolveSelectorOptions,
683
+ "allowInvalidThinkingSelectorFallback" | "credentialSessionId" | "modelRegistry" | "sessionId"
684
+ >,
685
+ ): PresetEquivalentAliasResolution {
686
+ const registry = options.modelRegistry;
687
+ const lookupAliasExists = registry?.lookupAliasExists?.bind(registry);
688
+ const resolveModelByLookupAlias = registry?.resolveModelByLookupAlias?.bind(registry);
689
+ if (!lookupAliasExists || !resolveModelByLookupAlias) {
690
+ return { explicitThinkingLevel: false };
691
+ }
692
+ const candidates = availableModels;
693
+ const sessionId = options.sessionId;
694
+
695
+ const tryAlias = (candidate: string): { model?: Model<Api>; knownButUnavailable: boolean } | undefined => {
696
+ if (candidate.includes("/")) return undefined;
697
+ const aliasKey = getFinalSlashSegmentAliasKey(candidate);
698
+ if (!aliasKey) return undefined;
699
+ if (!lookupAliasExists(aliasKey)) return undefined;
700
+ const model = resolveModelByLookupAlias(aliasKey, {
701
+ availableOnly: false,
702
+ candidates,
703
+ sessionId,
704
+ credentialSessionId: options.credentialSessionId,
705
+ });
706
+ return { model, knownButUnavailable: model === undefined };
707
+ };
708
+
709
+ // Full selector first — preserves route-suffix aliases without a thinking suffix.
710
+ const full = tryAlias(selector);
711
+ if (full) {
712
+ if (full.model) return { model: full.model, thinkingLevel: undefined, explicitThinkingLevel: false };
713
+ return { knownButUnavailable: true, explicitThinkingLevel: false };
714
+ }
715
+
716
+ // Then the base selector after splitting a single thinking suffix.
717
+ const suffix = splitSelectorThinkingSuffix(selector);
718
+ if (suffix.invalidSuffix !== undefined && !(options.allowInvalidThinkingSelectorFallback ?? true)) {
719
+ return { explicitThinkingLevel: false };
720
+ }
721
+ if (suffix.selector !== selector) {
722
+ const base = tryAlias(suffix.selector);
723
+ if (base) {
724
+ if (base.model) {
725
+ return {
726
+ model: base.model,
727
+ thinkingLevel: suffix.thinkingLevel,
728
+ explicitThinkingLevel: suffix.thinkingLevel !== undefined,
729
+ warning: suffix.thinkingLevel
730
+ ? undefined
731
+ : `Invalid thinking level "${suffix.invalidSuffix}" in pattern "${selector}". Using default instead.`,
732
+ };
733
+ }
734
+ return { knownButUnavailable: true, explicitThinkingLevel: false };
735
+ }
736
+ }
737
+
738
+ return { explicitThinkingLevel: false };
739
+ }
553
740
  function findGlobMatch(
554
741
  selector: string,
555
742
  candidates: Model<Api>[],
@@ -575,6 +762,8 @@ export function parseModelPattern(
575
762
  allowInvalidThinkingSelectorFallback?: boolean;
576
763
  modelRegistry?: CanonicalModelRegistry;
577
764
  sessionId?: string;
765
+ credentialSessionId?: string;
766
+ aliasIntent?: "preset-equivalent" | "reject";
578
767
  },
579
768
  ): ParsedModelResult {
580
769
  return resolveSelector(pattern, availableModels, { ...options, preferences });
@@ -690,6 +879,8 @@ export function resolveModelRoleValue(
690
879
  matchPreferences?: ModelMatchPreferences;
691
880
  modelRegistry?: CanonicalModelRegistry;
692
881
  sessionId?: string;
882
+ credentialSessionId?: string;
883
+ aliasIntent?: "preset-equivalent" | "reject";
693
884
  },
694
885
  ): ResolvedModelRoleValue {
695
886
  const effectivePatterns = normalizeModelPatternList(roleValue).flatMap(
@@ -704,6 +895,8 @@ export function resolveModelRoleValue(
704
895
  const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences, {
705
896
  modelRegistry: options?.modelRegistry,
706
897
  sessionId: options?.sessionId,
898
+ credentialSessionId: options?.credentialSessionId,
899
+ aliasIntent: options?.aliasIntent,
707
900
  });
708
901
  if (resolved.model) {
709
902
  return {
@@ -795,6 +988,7 @@ export function resolveModelOverride(
795
988
  modelRegistry: ModelLookupRegistry,
796
989
  settings?: Settings,
797
990
  sessionId?: string,
991
+ aliasIntent?: "preset-equivalent" | "reject",
798
992
  ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
799
993
  if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
800
994
  const availableModels = modelRegistry.getAvailable();
@@ -805,6 +999,7 @@ export function resolveModelOverride(
805
999
  matchPreferences,
806
1000
  modelRegistry,
807
1001
  sessionId,
1002
+ aliasIntent,
808
1003
  });
809
1004
  if (model) {
810
1005
  return { model, thinkingLevel, explicitThinkingLevel };
@@ -822,6 +1017,9 @@ export function resolveModelOverride(
822
1017
  */
823
1018
  export interface ModelChainResolutionOptions {
824
1019
  managedFallback?: boolean;
1020
+ aliasIntent?: "preset-equivalent" | "reject";
1021
+ canonicalSessionId?: string | null;
1022
+ credentialSessionId?: string;
825
1023
  }
826
1024
 
827
1025
  export async function resolveModelChainWithAuth(
@@ -840,30 +1038,54 @@ export async function resolveModelChainWithAuth(
840
1038
  const availableModels = modelRegistry.getAvailable();
841
1039
  const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
842
1040
  const skips: Array<{ selector: string; reason: string }> = [];
1041
+ const authFailedModels = new Set<string>();
1042
+ const canonicalSessionId =
1043
+ options?.canonicalSessionId === null ? undefined : (options?.canonicalSessionId ?? sessionId);
1044
+ const credentialSessionId = options?.credentialSessionId ?? sessionId;
843
1045
  for (let activeIndex = 0; activeIndex < modelPatterns.length; activeIndex += 1) {
844
1046
  const selector = modelPatterns[activeIndex];
845
- const candidate = resolveModelRoleValue(selector, availableModels, {
846
- settings,
847
- matchPreferences,
848
- modelRegistry,
849
- sessionId,
850
- });
851
- if (!candidate.model) {
852
- skips.push({ selector, reason: "unknown_model" });
853
- continue;
854
- }
855
- if (options?.managedFallback && modelPatterns.length > 1) {
856
- const cursorReason = managedCursorFallbackUnavailableReason(candidate.model, selector);
857
- if (cursorReason) {
858
- skips.push({ selector, reason: cursorReason });
859
- continue;
1047
+ const suffix = splitSelectorThinkingSuffix(selector);
1048
+ const aliasSelector = suffix.thinkingLevel ? suffix.selector : selector;
1049
+ const aliasKey = aliasSelector.includes("/") ? undefined : getFinalSlashSegmentAliasKey(aliasSelector);
1050
+ const retryEquivalentAlias =
1051
+ options?.aliasIntent === "preset-equivalent" &&
1052
+ aliasKey !== undefined &&
1053
+ (modelRegistry.lookupAliasExists?.(aliasKey) ?? false);
1054
+ let attemptedCandidate = false;
1055
+ while (true) {
1056
+ const candidates = availableModels.filter(
1057
+ model => !authFailedModels.has(`${model.provider}\u0000${model.id}`),
1058
+ );
1059
+ const candidate = resolveModelRoleValue(selector, candidates, {
1060
+ settings,
1061
+ matchPreferences,
1062
+ modelRegistry,
1063
+ sessionId: canonicalSessionId,
1064
+ credentialSessionId,
1065
+ aliasIntent: options?.aliasIntent,
1066
+ });
1067
+ if (!candidate.model) {
1068
+ if (!attemptedCandidate) skips.push({ selector, reason: "unknown_model" });
1069
+ break;
860
1070
  }
1071
+ if (options?.managedFallback && modelPatterns.length > 1) {
1072
+ const cursorReason = managedCursorFallbackUnavailableReason(candidate.model, selector);
1073
+ if (cursorReason) {
1074
+ skips.push({ selector, reason: cursorReason });
1075
+ break;
1076
+ }
1077
+ }
1078
+ const key = await modelRegistry.getApiKey(candidate.model, credentialSessionId);
1079
+ if (isAuthenticatedOrKeyless(key)) {
1080
+ return { ...candidate, activeIndex, skips };
1081
+ }
1082
+ skips.push({ selector, reason: "unauthenticated" });
1083
+ attemptedCandidate = true;
1084
+ authFailedModels.add(`${candidate.model.provider}\u0000${candidate.model.id}`);
1085
+ if (!retryEquivalentAlias) break;
1086
+ const stickySessionId = canonicalSessionId;
1087
+ if (stickySessionId) modelRegistry.clearCanonicalVariant?.(stickySessionId);
861
1088
  }
862
- const key = await modelRegistry.getApiKey(candidate.model, sessionId);
863
- if (isAuthenticatedOrKeyless(key)) {
864
- return { ...candidate, activeIndex, skips };
865
- }
866
- skips.push({ selector, reason: "unauthenticated" });
867
1089
  }
868
1090
  return { explicitThinkingLevel: false, activeIndex: modelPatterns.length, skips };
869
1091
  }
@@ -916,7 +1138,13 @@ export async function resolveModelOverrideWithAuthFallback(
916
1138
  let activeIndex = 0;
917
1139
  const canonicalScope = canonicalSessionId ?? authSessionId;
918
1140
  if (canonicalScope && parentActiveModelPattern) {
919
- const parentActiveModel = resolveModelOverride([parentActiveModelPattern], modelRegistry, settings).model;
1141
+ const parentActiveModel = resolveModelOverride(
1142
+ [parentActiveModelPattern],
1143
+ modelRegistry,
1144
+ settings,
1145
+ undefined,
1146
+ options?.aliasIntent,
1147
+ ).model;
920
1148
  if (parentActiveModel) {
921
1149
  modelRegistry.seedCanonicalVariant?.(canonicalScope, parentActiveModel);
922
1150
  }
@@ -927,6 +1155,8 @@ export async function resolveModelOverrideWithAuthFallback(
927
1155
  matchPreferences,
928
1156
  modelRegistry,
929
1157
  sessionId: canonicalScope,
1158
+ credentialSessionId: authSessionId,
1159
+ aliasIntent: options?.aliasIntent,
930
1160
  });
931
1161
  if (!requestedModel && candidate.model) {
932
1162
  requestedModel = candidate.model;
@@ -949,11 +1179,32 @@ export async function resolveModelOverrideWithAuthFallback(
949
1179
  if (isAuthenticatedOrKeyless(key)) {
950
1180
  return { ...candidate, requestedModel: candidate.model, authFallbackUsed: false, activeIndex, skips };
951
1181
  }
1182
+ if (
1183
+ options?.aliasIntent === "preset-equivalent" &&
1184
+ !splitSelectorThinkingSuffix(pattern).selector.includes("/")
1185
+ ) {
1186
+ const equivalentRetry = await resolveModelChainWithAuth([pattern], modelRegistry, settings, authSessionId, {
1187
+ managedFallback: options.managedFallback,
1188
+ aliasIntent: options.aliasIntent,
1189
+ canonicalSessionId: canonicalScope,
1190
+ });
1191
+ if (equivalentRetry.model) {
1192
+ return {
1193
+ model: equivalentRetry.model,
1194
+ thinkingLevel: equivalentRetry.thinkingLevel,
1195
+ explicitThinkingLevel: equivalentRetry.explicitThinkingLevel,
1196
+ requestedModel: candidate.model,
1197
+ authFallbackUsed: !modelsAreEqual(candidate.model, equivalentRetry.model),
1198
+ activeIndex,
1199
+ skips: [...skips, ...equivalentRetry.skips],
1200
+ };
1201
+ }
1202
+ }
952
1203
  skips.push({ selector: pattern, reason: "unauthenticated" });
953
1204
  activeIndex += 1;
954
1205
  }
955
1206
  const fallback = parentActiveModelPattern
956
- ? resolveModelOverride([parentActiveModelPattern], modelRegistry, settings, authSessionId)
1207
+ ? resolveModelOverride([parentActiveModelPattern], modelRegistry, settings, authSessionId, options?.aliasIntent)
957
1208
  : { explicitThinkingLevel: false };
958
1209
  if (fallback.model) {
959
1210
  const fallbackKey = await modelRegistry.getApiKey(fallback.model, authSessionId);
@@ -57,7 +57,14 @@ export const ModelCompatSchema = z.object({
57
57
  export const OpenAICompatSchema = ModelCompatSchema;
58
58
 
59
59
  export const GJC_MODEL_EFFORT_IDS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
60
- export const GJC_MODEL_ASSIGNMENT_TARGET_IDS = ["default", "executor", "architect", "planner", "critic"] as const;
60
+ export const GJC_MODEL_ASSIGNMENT_TARGET_IDS = [
61
+ "default",
62
+ "executor",
63
+ "architect",
64
+ "planner",
65
+ "critic",
66
+ "image",
67
+ ] as const;
61
68
  export const EffortSchema = z.enum(GJC_MODEL_EFFORT_IDS);
62
69
  const CacheRetentionSchema = z.enum(["none", "short", "long"]);
63
70
 
@@ -94,13 +101,13 @@ export const ModelBindingsSchema = z.object({
94
101
  });
95
102
 
96
103
  export const ProfileRoleSchema = z.enum(GJC_MODEL_ASSIGNMENT_TARGET_IDS);
97
- export const ProfileModelSelectorPattern = `^[^,/]+/[^,:]+(?::(?:${EffortSchema.options.join("|")}))?$`;
104
+ export const ProfileModelSelectorPattern = "^(?:[^,/]+/[^,]*[^,:]|[^/,]*[^/,:])$";
98
105
 
99
106
  export const ProfileModelSelectorSchema = z
100
107
  .string()
101
108
  .trim()
102
109
  .min(1)
103
- .regex(new RegExp(ProfileModelSelectorPattern), "Expected provider/modelId with optional :effort suffix");
110
+ .regex(new RegExp(ProfileModelSelectorPattern), "Expected modelId or provider/modelId with optional :effort suffix");
104
111
  export const ProfileModelMappingSchema = z.partialRecord(
105
112
  ProfileRoleSchema,
106
113
  stringOrNonEmptyArray(ProfileModelSelectorSchema),
@@ -108,7 +115,7 @@ export const ProfileModelMappingSchema = z.partialRecord(
108
115
 
109
116
  export const ProfileDefinitionSchema = z
110
117
  .object({
111
- required_providers: z.array(z.string().min(1)).min(1),
118
+ required_providers: z.array(z.string().min(1)),
112
119
  display_name: z.string().min(1).optional(),
113
120
  model_mapping: ProfileModelMappingSchema,
114
121
  })