evolcore 0.0.35 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +6 -0
- package/bin/install-codex-managed-hooks.mjs +20 -3
- package/dist/agents/baseagent-availability.js +1 -1
- package/dist/agents/baseagent.js +88 -0
- package/dist/agents/claude-runner.js +59 -12
- package/dist/agents/codex/app-server-client.js +18 -1
- package/dist/agents/codex-runner.js +262 -21
- package/dist/agents/ecagent-runner.js +47 -11
- package/dist/agents/gemini-runner.js +36 -12
- package/dist/aun/aid/client.js +4 -3
- package/dist/aun/aid/control-aid.js +270 -8
- package/dist/aun/aid/identity.js +6 -3
- package/dist/aun/aid/ownership-operation.js +58 -0
- package/dist/aun/aid/store.js +1 -0
- package/dist/aun/group/fs-download.js +67 -2
- package/dist/aun/group/history.js +20 -5
- package/dist/aun/group/operations.js +8 -2
- package/dist/aun/menu/client.js +39 -2
- package/dist/aun/menu/transport.js +1 -1
- package/dist/aun/msg/history.js +43 -4
- package/dist/aun/msg/managed-operation.js +30 -53
- package/dist/aun/msg/p2p.js +51 -15
- package/dist/aun/outbox.js +4 -0
- package/dist/channels/aun.js +260 -73
- package/dist/channels/daemon.js +6 -4
- package/dist/channels/feishu.js +3 -0
- package/dist/cli/agent-command.js +161 -223
- package/dist/cli/agent-create.js +190 -223
- package/dist/cli/agent.js +3 -1
- package/dist/cli/aid-bind-command.js +53 -0
- package/dist/cli/aun-commands.js +384 -91
- package/dist/cli/bench.js +1 -1
- package/dist/cli/cli-argv.js +74 -0
- package/dist/cli/daemon-commands.js +105 -74
- package/dist/cli/diag.js +1 -1
- package/dist/cli/fs-command.js +18 -19
- package/dist/cli/getmeta-command.js +159 -6
- package/dist/cli/index.js +6 -12
- package/dist/cli/init-channel.js +8 -55
- package/dist/cli/init.js +125 -218
- package/dist/cli/queue-command.js +11 -8
- package/dist/cli/restart-monitor.js +177 -85
- package/dist/cli/session-command.js +1 -1
- package/dist/cli/stats.js +7 -5
- package/dist/cli/thread-command.js +103 -30
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-msg-diag.js +1 -0
- package/dist/config/access-policy.js +2 -2
- package/dist/config/boot-log.js +14 -11
- package/dist/config/config-field-policy.js +9 -1
- package/dist/config/config-manager.js +176 -90
- package/dist/config/config-operation-service.js +7 -14
- package/dist/config/contact-book-store.js +142 -26
- package/dist/config/contact-book.js +4 -2
- package/dist/config/lifecycle.js +1 -1
- package/dist/config/owner-config.js +96 -0
- package/dist/config/peer-role-resolver.js +12 -18
- package/dist/config/providers-config.js +382 -0
- package/dist/config/providers-import.js +74 -0
- package/dist/config/providers-management.js +328 -0
- package/dist/config/providers.js +22 -0
- package/dist/config/resolved-config-op.js +17 -14
- package/dist/config/snapshot.js +28 -16
- package/dist/config-store.js +18 -14
- package/dist/core/agent-ipc-registration.js +21 -63
- package/dist/core/agent-runtime.js +16 -5
- package/dist/core/auth/agent-delegation.js +199 -25
- package/dist/core/auth/auth-gateway.js +44 -2
- package/dist/core/auth/authorization-denial.js +1 -1
- package/dist/core/auth/debug-context.js +2 -1
- package/dist/core/auth/delegation-authorization.js +2 -2
- package/dist/core/auth/delegation-diagnostics.js +4 -1
- package/dist/core/auth/operation-authorizer.js +10 -1
- package/dist/core/auth/operation-catalog.js +19 -1
- package/dist/core/bootstrap-service.js +43 -20
- package/dist/core/channel-loader.js +0 -1
- package/dist/core/channel-runtime.js +30 -4
- package/dist/core/command/agent-control.js +196 -37
- package/dist/core/command/cli-intent-parser.js +60 -4
- package/dist/core/command/command-handler.js +212 -60
- package/dist/core/command/connect-menu.js +1 -0
- package/dist/core/command/diag-service.js +5 -4
- package/dist/core/command/file-search.js +368 -0
- package/dist/core/command/menu-catalog.js +2 -1
- package/dist/core/command/menu-handler.js +292 -107
- package/dist/core/command/menu-protocol.js +23 -0
- package/dist/core/command/role-menu.js +7 -19
- package/dist/core/command/skill-slash.js +130 -25
- package/dist/core/command/slash-handler.js +101 -53
- package/dist/core/command/topic-slash.js +16 -5
- package/dist/core/daemon-bootstrap.js +7 -1
- package/dist/core/daemon-control.js +19 -1
- package/dist/core/daemon-lifecycle.js +2 -1
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/evolagent-registry.js +13 -2
- package/dist/core/evolagent.js +3 -6
- package/dist/core/handoff/ipc-registration.js +2 -2
- package/dist/core/handoff/runtime.js +40 -6
- package/dist/core/handoff/store.js +1 -0
- package/dist/core/inference/text-inference.js +95 -6
- package/dist/core/ipc-registration.js +43 -24
- package/dist/core/memory/consolidation-context.js +4 -1
- package/dist/core/memory/consolidation.js +25 -14
- package/dist/core/message/aun-ipc-sender.js +296 -88
- package/dist/core/message/aun-request-registry.js +226 -0
- package/dist/core/message/create-status.js +33 -4
- package/dist/core/message/message-bridge.js +62 -13
- package/dist/core/message/message-log.js +0 -1
- package/dist/core/message/message-queue.js +4 -1
- package/dist/core/message/message-utils.js +8 -1
- package/dist/core/message/peer-mode.js +17 -9
- package/dist/core/message/response-engine.js +266 -124
- package/dist/core/message/retry-scheduler.js +11 -8
- package/dist/core/model/catalog-service.js +60 -18
- package/dist/core/model/model-catalog.js +112 -9
- package/dist/core/model/model-fallback.js +94 -26
- package/dist/core/ownership/owner-resolver.js +687 -0
- package/dist/core/permission/approval-gateway.js +196 -62
- package/dist/core/permission/ec-command-parser.js +138 -25
- package/dist/core/permission/filesystem-approval-scope.js +141 -0
- package/dist/core/permission/protected-paths.js +14 -1
- package/dist/core/permission/sandbox-runtime.js +171 -18
- package/dist/core/permission/tool-policy.js +13 -2
- package/dist/core/role/runtime-policy.js +3 -4
- package/dist/core/session/session-idle-monitor.js +10 -8
- package/dist/core/session/session-manager.js +72 -45
- package/dist/core/session/session-mapper.js +18 -7
- package/dist/core/session/session-renew.js +5 -4
- package/dist/core/session/session-title.js +2 -1
- package/dist/core/session/session-turn-coordinator.js +4 -2
- package/dist/core/skill/index-generator.js +2 -16
- package/dist/core/task/task-recovery-service.js +15 -2
- package/dist/core/thread/command-parser.js +28 -13
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +11 -8
- package/dist/core/thread/thread-operation.js +79 -44
- package/dist/core/thread/thread-route.js +54 -6
- package/dist/core/thread/thread-service.js +236 -45
- package/dist/core/thread/topic-id.js +248 -0
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/eck/message-renderer.js +12 -2
- package/dist/index.js +50 -27
- package/dist/ipc.js +59 -6
- package/dist/paths.js +1 -0
- package/dist/product.js +1 -0
- package/dist/response-system/engines/v1/proactive-flow.js +10 -3
- package/dist/stats/db.js +8 -0
- package/dist/stats/writer.js +26 -4
- package/dist/trigger/authorization.js +48 -3
- package/dist/trigger/feedback.js +28 -10
- package/dist/trigger/ipc-service.js +86 -10
- package/dist/trigger/local-files.js +55 -23
- package/dist/trigger/scheduler.js +57 -11
- package/dist/trigger/system-definitions.js +1 -0
- package/dist/trigger/validation.js +44 -15
- package/dist/utils/aid-bind.js +17 -297
- package/dist/utils/avatar-upload.js +16 -4
- package/dist/utils/cross-platform.js +115 -36
- package/dist/utils/ecweb-supervisor.js +1 -1
- package/dist/utils/ecweb-utils.js +8 -1
- package/dist/utils/error-utils.js +44 -6
- package/dist/utils/instance-registry.js +12 -2
- package/dist/utils/npm-ops.js +102 -27
- package/dist/utils/package-version-info.js +72 -27
- package/dist/utils/restart-safety.js +50 -0
- package/dist/utils/upgrade-jobs.js +304 -0
- package/dist/utils/windows-autostart.js +17 -10
- package/kits/docs/INDEX.md +5 -0
- package/kits/docs/aun/ABILITY_CALLING.md +239 -0
- package/kits/docs/evolcore/INDEX.md +14 -3
- package/kits/docs/evolcore/agent-create.md +55 -141
- package/kits/docs/evolcore/agent.md +38 -16
- package/kits/docs/evolcore/aid.md +34 -0
- package/kits/docs/evolcore/config.md +20 -1
- package/kits/docs/evolcore/event.md +2 -2
- package/kits/docs/evolcore/fs.md +18 -2
- package/kits/docs/evolcore/group-collaboration.md +13 -246
- package/kits/docs/evolcore/group.md +2 -1
- package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
- package/kits/docs/evolcore/menu-protocol.md +65 -1
- package/kits/docs/evolcore/msg.md +64 -2
- package/kits/docs/evolcore/skill.md +86 -0
- package/kits/docs/evolcore/stats.md +6 -0
- package/kits/docs/evolcore/task.md +2 -0
- package/kits/docs/evolcore/thread-chain.md +145 -51
- package/kits/docs/evolcore/trigger.md +48 -3
- package/kits/eck_manifest.json +21 -0
- package/kits/eck_message_manifest.json +4 -4
- package/kits/migrations/migrate-role-config-v5.mjs +20 -7
- package/kits/rules/01-overview.md +2 -1
- package/kits/rules/03-identity.md +3 -1
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/07-memory-and-thread.md +1 -1
- package/kits/schemas/_meta.json +16 -1
- package/kits/schemas/agent-config.schema.13.json +44 -2
- package/kits/schemas/contact-book.schema.3.json +5 -0
- package/kits/schemas/daemon.schema.6.json +3 -1
- package/kits/schemas/daemon.schema.8.json +329 -0
- package/kits/schemas/defaults.schema.5.json +39 -3
- package/kits/schemas/providers.schema.1.json +76 -0
- package/kits/schemas/relation-config.schema.9.json +5 -1
- package/kits/skills/.switches.json +28 -0
- package/kits/skills/aid-ability/SKILL.md +214 -0
- package/kits/skills/ec-agent-create/SKILL.md +162 -0
- package/kits/skills/ec-group/SKILL.md +2 -1
- package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
- package/kits/skills/ec-identity/SKILL.md +143 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
- package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
- package/kits/skills/ec-trigger/SKILL.md +167 -0
- package/kits/skills/ec-trigger/references/examples.md +153 -0
- package/kits/skills/thread-ops/SKILL.md +108 -420
- package/kits/templates/message-fragments/thread-context.md +1 -2
- package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
- package/kits/templates/roles/member.json +1 -1
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +1 -0
- package/kits/templates/system-fragments/channel.md +2 -1
- package/kits/templates/system-fragments/commands.md +5 -4
- package/kits/templates/system-fragments/identity.md +2 -0
- package/kits/templates/system-fragments/session.md +1 -1
- package/kits/templates/system-fragments/venue.md +1 -1
- 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
|
-
|
|
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 =
|
|
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'
|
|
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
|
|
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
|
|
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 &&
|
|
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 (
|
|
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?.() ?? ''}:${
|
|
220
|
-
file(context) { const dir = this.options.storageDir?.(); return dir ? path.join(dir, `${
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
const
|
|
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
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|