evolcore 0.0.35 → 0.0.36

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 (224) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +134 -48
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +109 -34
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -6,16 +6,18 @@ export const API_RETRY_DELAYS_MS = [5_000, 10_000, 30_000];
6
6
  export const MAX_PROTOCOL_REPLAY_ATTEMPTS = 1;
7
7
  export class RetryScheduler {
8
8
  resolveModelId;
9
+ currentProvider;
9
10
  protocolReplayAttempts = 0;
10
11
  apiRetryAttempts = 0;
11
12
  modelFallbackAttempts = 0;
12
13
  attemptedModels = new Set();
13
14
  candidates;
14
- constructor(candidates, currentModel, resolveModelId = model => model) {
15
+ constructor(candidates, currentModel, resolveModelId = model => model, currentProvider) {
15
16
  this.resolveModelId = resolveModelId;
17
+ this.currentProvider = currentProvider;
16
18
  this.candidates = candidates;
17
19
  if (currentModel)
18
- this.attemptedModels.add(this.key(currentModel));
20
+ this.attemptedModels.add(this.key(currentModel, currentProvider));
19
21
  }
20
22
  get protocolReplays() {
21
23
  return this.protocolReplayAttempts;
@@ -26,8 +28,8 @@ export class RetryScheduler {
26
28
  shouldTimeoutAttempt() {
27
29
  return this.apiRetryAttempts > 0;
28
30
  }
29
- markModelAttempted(model) {
30
- this.attemptedModels.add(this.key(model));
31
+ markModelAttempted(model, provider = this.currentProvider) {
32
+ this.attemptedModels.add(this.key(model, provider));
31
33
  }
32
34
  afterProtocolFailure(reason, safeToReplay) {
33
35
  if (!reason || !safeToReplay)
@@ -42,10 +44,10 @@ export class RetryScheduler {
42
44
  };
43
45
  }
44
46
  afterModelUnavailable() {
45
- const candidate = this.candidates.find(item => !this.attemptedModels.has(this.key(item.model)));
47
+ const candidate = this.candidates.find(item => !this.attemptedModels.has(this.key(item.model, item.provider ?? this.currentProvider)));
46
48
  if (!candidate)
47
49
  return { kind: 'exhausted' };
48
- this.attemptedModels.add(this.key(candidate.model));
50
+ this.attemptedModels.add(this.key(candidate.model, candidate.provider ?? this.currentProvider));
49
51
  this.modelFallbackAttempts++;
50
52
  this.apiRetryAttempts = 0;
51
53
  return {
@@ -65,7 +67,8 @@ export class RetryScheduler {
65
67
  this.apiRetryAttempts++;
66
68
  return { kind: 'api_retry', attempt, maxAttempts: API_RETRY_DELAYS_MS.length, delayMs };
67
69
  }
68
- key(model) {
69
- return this.resolveModelId(model).trim().toLowerCase();
70
+ key(model, provider) {
71
+ const resolved = provider === this.currentProvider ? this.resolveModelId(model) : model;
72
+ return `${provider ?? '<legacy>'}:${resolved.trim().toLowerCase()}`;
70
73
  }
71
74
  }
@@ -14,8 +14,9 @@ export const CATALOG_ROUTE_TIMEOUT_MS = 15_000;
14
14
  * v2:保留上下文窗口后缀(`claude-opus-5[1m]` 不再被折叠成基础 ID)。
15
15
  * v3:保留 Codex 上游声明的 max effort,重新获取旧快照中过滤掉的能力。
16
16
  * v4:统一缺失 effort 元数据时的 transport fallback,并记录来源。
17
+ * v5:持久化 provider 原始目录,避免相同 provider/path 的不同服务风格相互污染。
17
18
  */
18
- export const CATALOG_SNAPSHOT_VERSION = 4;
19
+ export const CATALOG_SNAPSHOT_VERSION = 6;
19
20
  export const CONFIGURABLE_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
20
21
  export const TRANSPORT_EFFORTS = {
21
22
  claude: ['low', 'medium', 'high', 'xhigh', 'max'],
@@ -36,10 +37,18 @@ function stable(value) {
36
37
  export const catalogHash = (value) => createHash('sha256').update(stable(value)).digest('hex');
37
38
  export function catalogContextKey(context) {
38
39
  const headers = Object.fromEntries(Object.entries(context.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));
40
+ if (context.providerId)
41
+ return catalogHash({ providerId: context.providerId, baseUrl: context.baseUrl?.replace(/\/+$/, ''),
42
+ apiKey: context.apiKey, headers, queryParams: context.queryParams ?? {}, configUnavailable: context.configUnavailable });
39
43
  return catalogHash({ scope: context.scope, baseagent: context.baseagent, provider: context.provider,
40
44
  baseUrl: context.baseUrl?.replace(/\/+$/, ''), apiKey: context.apiKey, authType: context.authType,
41
45
  headers, queryParams: context.queryParams ?? {}, configUnavailable: context.configUnavailable });
42
46
  }
47
+ export function catalogStorageKey(context) {
48
+ if (context.providerId)
49
+ return catalogHash({ providerId: context.providerId, path: context.catalogPath ?? context.baseUrl?.replace(/\?.*$/u, '') ?? '' });
50
+ return catalogContextKey(context);
51
+ }
43
52
  const strings = (value) => Array.isArray(value)
44
53
  ? [...new Set(value.filter((v) => typeof v === 'string' && !!v.trim()).map(v => v.trim()))] : undefined;
45
54
  function rows(json) {
@@ -73,7 +82,9 @@ export function parseModelList(json, _url) {
73
82
  id, verification: 'listed',
74
83
  ...(typeof owner === 'string' ? { owned_by: owner } : {}),
75
84
  ...(typeof item?.created === 'number' ? { created: item.created } : {}),
76
- ...(typeof (item?.displayName ?? item?.display_name) === 'string' ? { displayName: item.displayName ?? item.display_name } : {}),
85
+ ...(typeof (item?.name ?? item?.displayName ?? item?.display_name) === 'string'
86
+ ? { displayName: item.name ?? item.displayName ?? item.display_name } : {}),
87
+ ...(typeof (item?.imageIn ?? item?.image_in) === 'boolean' ? { imageIn: item.imageIn ?? item.image_in } : {}),
77
88
  ...(levels ? { reasoningLevels: levels } : {}),
78
89
  ...(typeof (item?.defaultReasoningEffort ?? item?.default_reasoning_level) === 'string' ? { defaultReasoningLevel: item.defaultReasoningEffort ?? item.default_reasoning_level } : {}),
79
90
  ...(strings(item?.protocols ?? item?.supported_protocols) ? { protocols: strings(item.protocols ?? item.supported_protocols) } : {}),
@@ -127,7 +138,9 @@ function compatible(model, baseagent) {
127
138
  const protocols = { claude: ['anthropic', 'anthropic-messages', 'messages'], codex: ['openai-responses', 'responses'], ecagent: ['openai', 'openai-completions', 'openai-chat-completions', 'chat-completions'] };
128
139
  return !model.protocols || !protocols[baseagent] || model.protocols.some(p => protocols[baseagent].includes(p));
129
140
  }
130
- function adapt(models, baseagent) {
141
+ export function adaptCatalogModels(models, baseagent) {
142
+ if (!baseagent)
143
+ return models;
131
144
  const available = models.filter(m => compatible(m, baseagent)).map(m => {
132
145
  const reasoningLevels = configurableEfforts(baseagent, m.reasoningLevels);
133
146
  const effortSource = reasoningLevels.length === 0 ? 'none'
@@ -137,6 +150,22 @@ function adapt(models, baseagent) {
137
150
  const aliases = deriveAliases(available, baseagent);
138
151
  return [...Object.entries(aliases).filter(([id]) => !available.some(m => m.id === id)).map(([id, aliasFor]) => ({ ...available.find(m => m.id === aliasFor), id, aliasFor, owned_by: 'alias' })), ...available];
139
152
  }
153
+ /** Text services do not require Agent tools or aliases; only adapt effort metadata. */
154
+ export function adaptServiceCatalogModels(models, serviceType, style) {
155
+ if (serviceType !== 'text')
156
+ return models;
157
+ const transports = {
158
+ 'openai-compatible': 'ecagent',
159
+ claude: 'claude', anthropic: 'claude',
160
+ codex: 'codex', 'openai-responses': 'codex',
161
+ };
162
+ return models.map(model => {
163
+ const reasoningLevels = configurableEfforts(transports[style] ?? '', model.reasoningLevels);
164
+ const effortSource = !reasoningLevels.length ? 'none'
165
+ : model.reasoningLevels === undefined ? 'transport-fallback' : 'catalog';
166
+ return { ...model, reasoningLevels, effortSource };
167
+ });
168
+ }
140
169
  export function resolveCatalogModel(snapshot, model) { return snapshot.models.find(m => m.id === model)?.aliasFor ?? model; }
141
170
  export function catalogEfforts(snapshot, model, baseagent) {
142
171
  const entry = snapshot.models.find(m => m.id === resolveCatalogModel(snapshot, model));
@@ -154,7 +183,11 @@ class GatewayError extends Error {
154
183
  }
155
184
  }
156
185
  async function gatewayModels(context, fetcher) {
157
- const urls = [...new Set([appendV1RequestPath(context.baseUrl, 'models', context.queryParams), appendRequestPath(context.baseUrl, 'models', context.queryParams)])];
186
+ const configuredStyle = context.style ?? context.baseagent;
187
+ const style = configuredStyle === 'anthropic' ? 'claude' : configuredStyle === 'openai-responses' ? 'codex' : configuredStyle;
188
+ const urls = [...new Set(style === 'gemini' || context.providerId
189
+ ? [appendRequestPath(context.baseUrl, 'models', context.queryParams)]
190
+ : [appendV1RequestPath(context.baseUrl, 'models', context.queryParams), appendRequestPath(context.baseUrl, 'models', context.queryParams)])];
158
191
  let last;
159
192
  for (const url of urls) {
160
193
  try {
@@ -173,27 +206,29 @@ async function gatewayModels(context, fetcher) {
173
206
  throw new GatewayError();
174
207
  const signal = AbortSignal.timeout(Math.min(CATALOG_PAGE_TIMEOUT_MS, remaining));
175
208
  const headers = new Headers(context.headers);
176
- if (context.apiKey && !headers.has('authorization')) {
209
+ if (context.apiKey && style !== 'gemini' && !headers.has('authorization')) {
177
210
  headers.set('authorization', `Bearer ${context.apiKey}`);
178
211
  }
179
- if (context.apiKey && context.baseagent === 'claude' && !headers.has('x-api-key')) {
212
+ if (context.apiKey && style === 'gemini' && !headers.has('x-goog-api-key'))
213
+ headers.set('x-goog-api-key', context.apiKey);
214
+ if (context.apiKey && style === 'claude' && !headers.has('x-api-key')) {
180
215
  headers.set('x-api-key', context.apiKey);
181
216
  }
182
- if (context.baseagent === 'claude' && !headers.has('anthropic-version'))
217
+ if (style === 'claude' && !headers.has('anthropic-version'))
183
218
  headers.set('anthropic-version', '2023-06-01');
184
219
  const response = await fetcher(next, { signal, headers, redirect: 'error' });
185
220
  if (!response.ok)
186
221
  throw new GatewayError(response.status);
187
222
  const json = await response.json();
188
- models.push(...parseModelList(json));
189
- const cursor = json?.nextCursor ?? json?.next_cursor ?? (json?.has_more ? json?.last_id : undefined);
223
+ models.push(...parseModelList(json).map(model => style === 'gemini' ? { ...model, id: model.id.replace(/^models\//u, '') } : model));
224
+ const cursor = json?.nextPageToken ?? json?.nextCursor ?? json?.next_cursor ?? (json?.has_more ? json?.last_id : undefined);
190
225
  if (!cursor) {
191
226
  if (json?.has_more)
192
227
  throw new GatewayError();
193
228
  return [...new Map(models.map(m => [m.id, m])).values()];
194
229
  }
195
230
  const parsed = new URL(url);
196
- parsed.searchParams.set(json?.has_more && !json?.nextCursor && !json?.next_cursor ? 'after_id' : 'cursor', String(cursor));
231
+ parsed.searchParams.set(json?.nextPageToken ? 'pageToken' : json?.has_more && !json?.nextCursor && !json?.next_cursor ? 'after_id' : 'cursor', String(cursor));
197
232
  next = parsed.toString();
198
233
  }
199
234
  throw new GatewayError();
@@ -216,8 +251,8 @@ export class ModelCatalogService {
216
251
  this.options = options;
217
252
  }
218
253
  now() { return (this.options.now ?? Date.now)(); }
219
- key(context) { return `${this.options.storageDir?.() ?? ''}:${catalogContextKey(context)}`; }
220
- file(context) { const dir = this.options.storageDir?.(); return dir ? path.join(dir, `${catalogContextKey(context)}.json`) : undefined; }
254
+ key(context) { return `${this.options.storageDir?.() ?? ''}:${catalogStorageKey(context)}`; }
255
+ file(context) { const dir = this.options.storageDir?.(); return dir ? path.join(dir, `${catalogStorageKey(context)}.json`) : undefined; }
221
256
  previous(context) {
222
257
  const key = this.key(context);
223
258
  let snapshot = this.successes.get(key);
@@ -236,7 +271,10 @@ export class ModelCatalogService {
236
271
  return snapshot;
237
272
  }
238
273
  peek(context) {
239
- return structuredClone(this.latest.get(this.key(context)) ?? this.previous(context) ?? this.make(context, 'builtin', 'fallback', this.legacyBuiltins(context), [{ code: 'BUILTIN', message: '内置候选,未经当前连接验证' }]));
274
+ const fallback = context.providerId
275
+ ? this.make(context, 'gateway', 'fallback', [], [{ code: 'GATEWAY_UNAVAILABLE', message: '当前服务商尚无成功模型目录' }])
276
+ : this.make(context, 'builtin', 'fallback', this.legacyBuiltins(context), [{ code: 'BUILTIN', message: '内置候选,未经当前连接验证' }]);
277
+ return this.present(structuredClone(this.latest.get(this.key(context)) ?? this.previous(context) ?? fallback), context);
240
278
  }
241
279
  async get(context, refresh = false) {
242
280
  const key = this.key(context);
@@ -257,18 +295,20 @@ export class ModelCatalogService {
257
295
  // while each read also asks the shared background job to refresh it.
258
296
  // Explicit refresh callers still await that same in-flight request.
259
297
  void pending.catch(() => undefined);
260
- return structuredClone(previous);
298
+ return this.present(structuredClone(previous), context);
261
299
  }
262
- return structuredClone(await pending);
300
+ return this.present(structuredClone(await pending), context);
263
301
  }
264
302
  clear() { this.latest.clear(); this.successes.clear(); }
265
303
  make(context, source, freshness, models, diagnostics) {
266
- const entries = adapt(models, context.baseagent);
267
304
  const fetchedAt = new Date(this.now()).toISOString();
268
305
  // A revision identifies the connection's model/capability set. Fetch time
269
306
  // belongs to freshness metadata and must not make an unchanged catalog
270
307
  // look like a different resource on every refresh.
271
- return { revision: catalogHash({ connection: catalogContextKey(context), source, entries }), source, freshness, fetchedAt, models: entries, diagnostics };
308
+ return { revision: catalogHash({ connection: catalogContextKey(context), source, models }), source, freshness, fetchedAt, models, diagnostics };
309
+ }
310
+ present(snapshot, context) {
311
+ return { ...snapshot, models: adaptCatalogModels(snapshot.models, context.baseagent) };
272
312
  }
273
313
  save(context, snapshot) {
274
314
  this.successes.set(this.key(context), snapshot);
@@ -306,7 +346,7 @@ export class ModelCatalogService {
306
346
  try {
307
347
  const models = await gatewayModels(context, this.options.fetch ?? fetch);
308
348
  // Enrich only IDs the gateway listed. Native models never add/remove membership.
309
- const native = context.baseagent === 'codex' && models.length ? await this.options.native?.(context).catch(() => null) : null;
349
+ const native = !context.providerId && context.baseagent === 'codex' && models.length ? await this.options.native?.(context).catch(() => null) : null;
310
350
  const metadata = new Map((native ?? []).map(m => [m.id, m]));
311
351
  const enriched = models.map(m => ({ ...m, ...(m.reasoningLevels === undefined && metadata.get(m.id)?.reasoningLevels !== undefined ? { reasoningLevels: metadata.get(m.id).reasoningLevels } : {}) }));
312
352
  const result = this.make(context, 'gateway', 'fresh', enriched, diagnostics);
@@ -325,6 +365,8 @@ export class ModelCatalogService {
325
365
  }
326
366
  if (previous)
327
367
  return { ...previous, freshness: 'stale', diagnostics: [...diagnostics, { code: 'STALE_CACHE', message: '使用同一连接的最近成功目录' }] };
368
+ if (context.providerId)
369
+ return this.make(context, 'gateway', 'fallback', [], diagnostics);
328
370
  if (!context.configUnavailable && this.options.native) {
329
371
  try {
330
372
  const native = await this.options.native(context);
@@ -11,13 +11,14 @@ import { loadDefaults } from '../../config-store.js';
11
11
  import net from 'node:net';
12
12
  import fs from 'node:fs';
13
13
  import { getWindowsInstanceSocketPathCandidates, resolvePaths } from '../../paths.js';
14
- import { ModelCatalogService, catalogEfforts, resolveCatalogModel } from './catalog-service.js';
14
+ import { ModelCatalogService, adaptCatalogModels, adaptServiceCatalogModels, catalogEfforts, catalogHash, resolveCatalogModel } from './catalog-service.js';
15
+ import { providerIsConfigured, providerServiceTypeForBaseagent, providerServiceTypesForBaseagent, resolveProviderService, resolveProviderServiceForBaseagent } from '../../config/providers.js';
15
16
  export * from './catalog-service.js';
16
17
  let daemonOwner = false;
17
18
  export function setModelCatalogDaemonOwner(value) { daemonOwner = value; }
18
19
  /** Only a refused/missing socket means offline. A busy/rejecting daemon must
19
20
  * never cause another process to create an independent online catalog. */
20
- export async function queryDaemonCatalog(self, baseagent, refresh = false) {
21
+ export async function queryDaemonCatalog(self, baseagent, refresh = false, selection) {
21
22
  const paths = resolvePaths();
22
23
  let controlToken;
23
24
  try {
@@ -41,7 +42,7 @@ export async function queryDaemonCatalog(self, baseagent, refresh = false) {
41
42
  resolve(value);
42
43
  };
43
44
  connection.setTimeout(25_000, () => finish(new Error('模型目录 daemon 查询超时')));
44
- connection.on('connect', () => connection.write(JSON.stringify({ type: 'model.catalog', self, baseagent, refresh, controlToken }) + '\n'));
45
+ connection.on('connect', () => connection.write(JSON.stringify({ type: 'model.catalog', self, baseagent, refresh, ...selection, controlToken }) + '\n'));
45
46
  connection.on('error', (error) => {
46
47
  if (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')
47
48
  finish();
@@ -77,12 +78,72 @@ const service = new ModelCatalogService({
77
78
  legacyAliasFile: () => resolvePaths().claudeModelCache,
78
79
  native: async (context) => (await import('../../agents/codex/catalog.js')).codexNativeCatalog(context),
79
80
  });
81
+ const HOST_BASEAGENTS = ['claude', 'codex', 'ecagent', 'gemini'];
82
+ function resolveProviderCatalogBaseagent(serviceType, requested) {
83
+ const baseagent = requested ?? (['claude', 'codex', 'gemini'].includes(serviceType) ? serviceType : '');
84
+ if (baseagent && (!HOST_BASEAGENTS.includes(baseagent)
85
+ || !providerServiceTypesForBaseagent(baseagent).includes(serviceType))) {
86
+ throw new Error(`Baseagent ${baseagent} is incompatible with provider service ${serviceType}`);
87
+ }
88
+ return baseagent;
89
+ }
90
+ export async function getProviderCatalog(providerId, serviceType, refresh = false, management = false, requestedBaseagent) {
91
+ const selected = requestedBaseagent
92
+ ? resolveProviderServiceForBaseagent(providerId, requestedBaseagent, serviceType)
93
+ : resolveProviderService(providerId, serviceType);
94
+ const effectiveServiceType = selected.serviceType;
95
+ const baseagent = resolveProviderCatalogBaseagent(effectiveServiceType, requestedBaseagent);
96
+ const present = (snapshot) => {
97
+ if (management)
98
+ return snapshot;
99
+ const overridden = applyProviderModelOverrides(snapshot, selected.modelOverrides);
100
+ return { ...overridden, models: baseagent
101
+ ? adaptCatalogModels(overridden.models, baseagent)
102
+ : adaptServiceCatalogModels(overridden.models, effectiveServiceType, selected.style) };
103
+ };
104
+ if (!daemonOwner) {
105
+ const snapshot = await queryDaemonCatalog(undefined, undefined, refresh, { providerId, serviceType: effectiveServiceType });
106
+ if (snapshot)
107
+ return present(snapshot);
108
+ }
109
+ const provider = selected;
110
+ const snapshot = await service.get({ ...provider, scope: '<providers>', baseagent: '' }, refresh);
111
+ return present(snapshot);
112
+ }
113
+ export async function getLocalProviderCatalog(providerId, serviceType, refresh = false) {
114
+ const provider = resolveProviderService(providerId, serviceType);
115
+ return service.get({ ...provider, scope: '<providers>', baseagent: '' }, refresh);
116
+ }
117
+ export function previewProviderCatalog(provider, requestedBaseagent) {
118
+ const baseagent = resolveProviderCatalogBaseagent(provider.serviceType, requestedBaseagent);
119
+ return new ModelCatalogService().get({ ...provider, scope: '<provider-preview>', baseagent: '' }, true)
120
+ .then(snapshot => ({ ...snapshot, models: baseagent
121
+ ? adaptCatalogModels(snapshot.models, baseagent)
122
+ : adaptServiceCatalogModels(snapshot.models, provider.serviceType, provider.style) }));
123
+ }
80
124
  export function resolveCatalogContext(self, ba) {
81
125
  const baseagent = ba || activeBaseagent(self);
82
126
  const context = { scope: self || '<defaults>', baseagent };
127
+ const config = self ? resolveEffective({ self }, { cache: true, expand: true }) : { baseagents: loadDefaults()?.baseagents ?? {} };
128
+ const block = (config.baseagents ?? {});
129
+ const selected = block[baseagent];
130
+ if (providerIsConfigured(selected)) {
131
+ const provider = resolveProviderServiceForBaseagent(selected.provider, baseagent, selected.providerServiceType);
132
+ Object.assign(context, {
133
+ provider: provider.providerId,
134
+ providerId: provider.providerId,
135
+ providerServiceType: provider.serviceType,
136
+ catalogPath: provider.catalogPath,
137
+ style: provider.style,
138
+ baseUrl: provider.baseUrl,
139
+ apiKey: provider.apiKey,
140
+ headers: provider.headers,
141
+ queryParams: provider.queryParams,
142
+ modelOverrides: provider.modelOverrides,
143
+ });
144
+ return context;
145
+ }
83
146
  try {
84
- const config = self ? resolveEffective({ self }, { cache: true, expand: true }) : { baseagents: loadDefaults()?.baseagents ?? {} };
85
- const block = (config.baseagents ?? {});
86
147
  const cfg = { agents: block };
87
148
  const resolved = baseagent === 'codex' ? resolveOpenaiConfig(cfg, block.codex)
88
149
  : baseagent === 'claude' ? resolveAnthropicConfig(cfg, block.claude)
@@ -106,7 +167,6 @@ export function resolveCatalogContext(self, ba) {
106
167
  }
107
168
  return context;
108
169
  }
109
- const HOST_BASEAGENTS = ['claude', 'codex', 'ecagent', 'gemini'];
110
170
  /** Query the catalog for a host-level default connection, without Agent scope. */
111
171
  export async function getBaseagentCatalog(baseagent, options = {}) {
112
172
  if (!HOST_BASEAGENTS.includes(baseagent)) {
@@ -118,7 +178,8 @@ export async function getBaseagentCatalog(baseagent, options = {}) {
118
178
  }
119
179
  /** Used by the IPC handler and the owner process. Never calls IPC recursively. */
120
180
  export async function getLocalCatalog(self, ba, refresh = false) {
121
- return service.get(resolveCatalogContext(self, ba), refresh);
181
+ const context = resolveCatalogContext(self, ba);
182
+ return applyProviderModelOverrides(await service.get(context, refresh), context.modelOverrides);
122
183
  }
123
184
  export async function getCatalog(self, ba, refresh = false) {
124
185
  if (!daemonOwner) {
@@ -132,16 +193,58 @@ export async function getCatalog(self, ba, refresh = false) {
132
193
  export function getRunnerCatalog(baseagent, self, connection) {
133
194
  if (self)
134
195
  return getCatalog(self, baseagent);
135
- return service.get({ ...connection, scope: '<defaults>', baseagent });
196
+ if (connection.providerId !== undefined) {
197
+ return getProviderCatalog(connection.providerId, connection.providerServiceType ?? providerServiceTypeForBaseagent(baseagent), false, false, baseagent);
198
+ }
199
+ const context = { ...connection, scope: '<defaults>', baseagent };
200
+ return service.get(context).then(snapshot => applyProviderModelOverrides(snapshot, context.modelOverrides));
136
201
  }
137
202
  export function peekRunnerCatalog(baseagent, self, connection) {
138
- return service.peek(self ? resolveCatalogContext(self, baseagent) : { ...connection, scope: '<defaults>', baseagent });
203
+ const context = self ? resolveCatalogContext(self, baseagent) : {
204
+ ...(connection.providerId !== undefined ? resolveProviderServiceForBaseagent(connection.providerId, baseagent, connection.providerServiceType) : connection), scope: '<defaults>', baseagent,
205
+ };
206
+ return applyProviderModelOverrides(service.peek(context), context.modelOverrides);
139
207
  }
140
208
  export async function getModelEfforts(model, self, ba) {
141
209
  const baseagent = ba || activeBaseagent(self);
142
210
  return catalogEfforts(await getCatalog(self, baseagent), model, baseagent);
143
211
  }
144
212
  export function clearModelCatalogCache() { service.clear(); }
213
+ export function applyProviderModelOverrides(snapshot, overrides) {
214
+ if (!overrides || Object.keys(overrides).length === 0)
215
+ return snapshot;
216
+ const disabled = new Set(Object.entries(overrides)
217
+ .filter(([, value]) => value.enabled === false)
218
+ .map(([modelId]) => modelId));
219
+ const listed = new Set(snapshot.models.map(model => model.id));
220
+ const manualModels = Object.entries(overrides)
221
+ .filter(([id, override]) => override.manual === true && !listed.has(id) && override.enabled !== false)
222
+ .map(([id, override]) => ({
223
+ id,
224
+ verification: 'unverified',
225
+ manual: true,
226
+ ...(typeof override.name === 'string' && override.name ? { displayName: override.name } : {}),
227
+ ...(typeof override.imageIn === 'boolean' ? { imageIn: override.imageIn } : {}),
228
+ }));
229
+ const models = [...snapshot.models, ...manualModels]
230
+ .filter(model => !disabled.has(model.aliasFor ?? model.id))
231
+ .map(model => {
232
+ const override = overrides[model.aliasFor ?? model.id];
233
+ if (!override)
234
+ return model;
235
+ const result = { ...model };
236
+ if (override.name === '')
237
+ delete result.displayName;
238
+ else if (typeof override.name === 'string')
239
+ result.displayName = override.name;
240
+ if (typeof override.imageIn === 'boolean')
241
+ result.imageIn = override.imageIn;
242
+ if (override.manual === true)
243
+ result.manual = true;
244
+ return result;
245
+ });
246
+ return { ...snapshot, revision: catalogHash({ revision: snapshot.revision, models }), models };
247
+ }
145
248
  export function modelMatches(agent, candidate, current) {
146
249
  if (!current)
147
250
  return false;
@@ -1,5 +1,6 @@
1
- import { catalogEfforts, getCatalog, resolveCatalogModel } from './model-catalog.js';
2
- import { ConfigTarget, read } from '../../config/config-manager.js';
1
+ import { catalogEfforts, getCatalog, getProviderCatalog, resolveCatalogModel } from './model-catalog.js';
2
+ import { ConfigTarget, read, resolveEffective } from '../../config/config-manager.js';
3
+ import { providerServiceTypeForBaseagent, providerServiceTypeForPurpose, resolveServiceSelection } from '../../config/providers.js';
3
4
  export const MAX_FALLBACK_MODELS = 3;
4
5
  function isRecord(value) {
5
6
  return !!value && typeof value === 'object' && !Array.isArray(value);
@@ -7,21 +8,22 @@ function isRecord(value) {
7
8
  function normalizeCandidate(value) {
8
9
  if (!isRecord(value) || typeof value.model !== 'string')
9
10
  return undefined;
11
+ if (Object.keys(value).some(key => !['provider', 'model', 'effort'].includes(key)))
12
+ return undefined;
13
+ if (value.provider !== undefined && (typeof value.provider !== 'string' || !value.provider.trim()))
14
+ return undefined;
10
15
  const model = value.model.trim();
11
16
  if (!model)
12
17
  return undefined;
13
18
  if (value.effort !== undefined && (typeof value.effort !== 'string' || !value.effort.trim()))
14
19
  return undefined;
15
20
  return {
21
+ ...(typeof value.provider === 'string' ? { provider: value.provider.trim() } : {}),
16
22
  model,
17
23
  ...(typeof value.effort === 'string' ? { effort: value.effort.trim() } : {}),
18
24
  };
19
25
  }
20
- /** Read only defaults.json: fallback policy must not participate in H-chain merging. */
21
- export function configuredFallbackModels(baseagent) {
22
- const defaults = read(ConfigTarget.Defaults, undefined, { cache: true });
23
- const block = isRecord(defaults?.baseagents) ? defaults.baseagents[baseagent] : undefined;
24
- const raw = isRecord(block) ? block.fallbackModels : undefined;
26
+ function configuredFallbacks(raw) {
25
27
  if (!Array.isArray(raw))
26
28
  return { candidates: [], rejected: [] };
27
29
  const candidates = [];
@@ -40,35 +42,101 @@ export function configuredFallbackModels(baseagent) {
40
42
  }
41
43
  return { candidates, rejected };
42
44
  }
43
- export async function eligibleFallbackModels(input) {
44
- const configured = configuredFallbackModels(input.baseagent);
45
+ export function configuredFallbackModels(baseagent) {
46
+ const defaults = read(ConfigTarget.Defaults, undefined, { cache: true });
47
+ const block = isRecord(defaults?.baseagents) ? defaults.baseagents[baseagent] : undefined;
48
+ return configuredFallbacks(isRecord(block) ? block.fallback : undefined);
49
+ }
50
+ async function eligibleCandidates(input) {
45
51
  const candidates = [];
46
- const rejected = [...configured.rejected];
47
- if (!configured.candidates.length)
52
+ const rejected = [...input.configured.rejected];
53
+ if (!input.configured.candidates.length)
48
54
  return { candidates, rejected };
49
- const catalog = await (input.catalog?.() ?? getCatalog(input.selfAid, input.baseagent));
50
- const resolve = (model) => resolveCatalogModel(catalog, model);
51
- const key = (model) => resolve(model).trim();
52
- const seen = new Set();
53
- if (input.currentModel)
54
- seen.add(key(input.currentModel));
55
- for (const candidate of configured.candidates) {
56
- const resolvedModel = resolve(candidate.model);
57
- const candidateKey = key(candidate.model);
58
- if (seen.has(candidateKey)) {
59
- rejected.push({ model: candidate.model, reason: candidateKey === key(input.currentModel ?? '') ? 'current_model' : 'duplicate' });
55
+ const catalogs = new Map();
56
+ const loadCatalog = (provider) => {
57
+ const key = provider ?? '<legacy>';
58
+ if (!catalogs.has(key)) {
59
+ const serviceType = provider
60
+ ? input.catalogProviderServiceType ?? (input.catalogBaseagent
61
+ ? providerServiceTypeForBaseagent(input.catalogBaseagent)
62
+ : input.serviceType)
63
+ : input.serviceType;
64
+ catalogs.set(key, provider
65
+ ? getProviderCatalog(provider, serviceType, false, false, input.catalogBaseagent)
66
+ : input.catalog?.() ?? getCatalog(input.selfAid, input.serviceType));
67
+ }
68
+ return catalogs.get(key);
69
+ };
70
+ const identity = (provider, model) => `${provider ?? '<legacy>'}:${model.trim().toLowerCase()}`;
71
+ let primaryIdentity = input.currentModel ? identity(input.currentProvider, input.currentModel) : undefined;
72
+ if (input.currentModel) {
73
+ try {
74
+ const primaryCatalog = await loadCatalog(input.currentProvider);
75
+ primaryIdentity = identity(input.currentProvider, resolveCatalogModel(primaryCatalog, input.currentModel));
76
+ }
77
+ catch { }
78
+ }
79
+ const seen = new Set(primaryIdentity ? [primaryIdentity] : []);
80
+ for (const candidate of input.configured.candidates) {
81
+ const provider = candidate.provider ?? input.currentProvider;
82
+ let catalog;
83
+ try {
84
+ catalog = await loadCatalog(provider);
85
+ }
86
+ catch {
87
+ rejected.push({ model: candidate.model, reason: 'not_allowed' });
60
88
  continue;
61
89
  }
62
- if (!input.isAllowed(resolvedModel)) {
90
+ const resolvedModel = resolveCatalogModel(catalog, candidate.model);
91
+ const key = identity(provider, resolvedModel);
92
+ if (seen.has(key)) {
93
+ rejected.push({ model: candidate.model, reason: key === primaryIdentity ? 'current_model' : 'duplicate' });
94
+ continue;
95
+ }
96
+ const listed = catalog.models.some(entry => entry.id === candidate.model
97
+ || entry.id === resolvedModel
98
+ || entry.aliasFor === candidate.model
99
+ || entry.aliasFor === resolvedModel);
100
+ if (!listed || !input.isAllowed(resolvedModel)) {
63
101
  rejected.push({ model: candidate.model, reason: 'not_allowed' });
64
102
  continue;
65
103
  }
66
- if (candidate.effort && !catalogEfforts(catalog, resolvedModel, input.baseagent).includes(candidate.effort)) {
104
+ const efforts = input.serviceType === 'text'
105
+ ? catalog.models.find(entry => entry.id === resolvedModel)?.reasoningLevels ?? []
106
+ : catalogEfforts(catalog, resolvedModel, input.serviceType);
107
+ if (candidate.effort && !efforts.includes(candidate.effort)) {
67
108
  rejected.push({ model: candidate.model, reason: 'unsupported_effort' });
68
109
  continue;
69
110
  }
70
- seen.add(candidateKey);
71
- candidates.push({ ...candidate, model: resolvedModel });
111
+ seen.add(key);
112
+ candidates.push({ ...candidate, ...(provider ? { provider } : {}), model: resolvedModel });
72
113
  }
73
114
  return { candidates, rejected };
74
115
  }
116
+ export async function eligibleFallbackModels(input) {
117
+ const effective = input.selfAid
118
+ ? resolveEffective({ self: input.selfAid }, { cache: true, expand: true })
119
+ : read(ConfigTarget.Defaults, undefined, { cache: true });
120
+ const selection = effective?.baseagents?.[input.baseagent];
121
+ return eligibleCandidates({
122
+ ...input,
123
+ configured: configuredFallbackModels(input.baseagent),
124
+ serviceType: input.baseagent,
125
+ catalogBaseagent: input.baseagent,
126
+ catalogProviderServiceType: selection?.providerServiceType,
127
+ });
128
+ }
129
+ export async function eligibleServiceFallbackModels(input) {
130
+ const selection = input.selections?.[input.purpose];
131
+ const primary = resolveServiceSelection(input.selections, input.purpose);
132
+ if (!selection || !primary)
133
+ return { candidates: [], rejected: [] };
134
+ return eligibleCandidates({
135
+ configured: configuredFallbacks(selection.fallback),
136
+ selfAid: input.selfAid,
137
+ serviceType: providerServiceTypeForPurpose(input.purpose),
138
+ currentModel: primary.model,
139
+ currentProvider: primary.providerId,
140
+ isAllowed: () => true,
141
+ });
142
+ }