@gajae-code/coding-agent 0.12.6 → 0.12.8
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 +22 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +8 -10
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +31 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/artifacts.d.ts +5 -8
- package/dist/types/session/client-bridge.d.ts +0 -14
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +7 -6
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +0 -32
- package/dist/types/task/receipt.d.ts +1 -2
- package/dist/types/task/types.d.ts +43 -178
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +0 -3
- package/dist/types/tools/output-meta.d.ts +5 -37
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/dist/types/tools/tool-result.d.ts +0 -1
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/async/job-manager.ts +430 -46
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/cursor.ts +13 -20
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +151 -425
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
- package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
- package/src/gjc-runtime/workflow-manifest.ts +0 -12
- package/src/internal-urls/agent-protocol.ts +38 -98
- package/src/internal-urls/artifact-protocol.ts +17 -28
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +464 -50
- package/src/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -8
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +105 -359
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +431 -130
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/host.ts +9 -187
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/host/reverse-leases.ts +42 -249
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +10 -7
- package/src/session/agent-session.ts +59 -202
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +362 -124
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +48 -15
- package/src/session/session-storage.ts +72 -5
- package/src/session/streaming-output.ts +40 -198
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +237 -65
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +239 -818
- package/src/tools/eval.ts +4 -99
- package/src/tools/fetch.ts +15 -77
- package/src/tools/find.ts +3 -7
- package/src/tools/output-meta.ts +114 -696
- package/src/tools/search.ts +4 -9
- package/src/tools/subagent.ts +97 -75
- package/src/tools/telegram-send.ts +4 -2
- package/src/tools/todo-write.ts +47 -0
- package/src/tools/tool-result.ts +0 -1
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
- package/dist/types/utils/artifact-id.d.ts +0 -2
- package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
- package/src/utils/artifact-id.ts +0 -6
|
@@ -37,6 +37,11 @@ import type { OAuthCredentials, OAuthLoginCallbacks } from "@gajae-code/ai/utils
|
|
|
37
37
|
import { $pickCredentialEnv, isRecord, logger } from "@gajae-code/utils";
|
|
38
38
|
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
39
39
|
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
40
|
+
import {
|
|
41
|
+
type ActiveProviderDescriptor,
|
|
42
|
+
ActiveProviderResolutionError,
|
|
43
|
+
projectActiveProviderDescriptors,
|
|
44
|
+
} from "../sdk/providers";
|
|
40
45
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
41
46
|
import type { ActiveSearchModelContext, WebSearchMode } from "../web/search/types";
|
|
42
47
|
import { type ConfigError, ConfigFile } from "./config-file";
|
|
@@ -77,6 +82,20 @@ export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant,
|
|
|
77
82
|
export { isAuthenticated, kNoAuth };
|
|
78
83
|
|
|
79
84
|
const MAX_SESSION_CANONICAL_VARIANTS = 64;
|
|
85
|
+
function redactDiscoveryUrl(value: string | URL): string {
|
|
86
|
+
try {
|
|
87
|
+
const url = typeof value === "string" ? new URL(value) : value;
|
|
88
|
+
return `${url.origin}${url.pathname}`;
|
|
89
|
+
} catch {
|
|
90
|
+
return "(invalid URL)";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function stripUrlQuery(value: string): string {
|
|
94
|
+
const queryStart = value.indexOf("?");
|
|
95
|
+
if (queryStart < 0) return value;
|
|
96
|
+
const fragmentStart = value.indexOf("#", queryStart);
|
|
97
|
+
return value.slice(0, queryStart) + (fragmentStart < 0 ? "" : value.slice(fragmentStart));
|
|
98
|
+
}
|
|
80
99
|
|
|
81
100
|
function envAvailabilityFingerprint(): string {
|
|
82
101
|
return Object.entries(process.env)
|
|
@@ -1014,6 +1033,24 @@ function getConfiguredProviderOrderFromSettings(): string[] {
|
|
|
1014
1033
|
return [];
|
|
1015
1034
|
}
|
|
1016
1035
|
}
|
|
1036
|
+
interface ProviderActivityEvidence {
|
|
1037
|
+
staticModelIds: ReadonlySet<string>;
|
|
1038
|
+
staticConfigured: boolean;
|
|
1039
|
+
discoveryConfigured: boolean;
|
|
1040
|
+
implicitDiscovery: boolean;
|
|
1041
|
+
descriptorBacked: boolean;
|
|
1042
|
+
descriptorFresh: boolean;
|
|
1043
|
+
descriptorModelIds: ReadonlySet<string>;
|
|
1044
|
+
authGeneration: string;
|
|
1045
|
+
endpoint: string;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
interface ModelManagerDiscoveryOptions {
|
|
1049
|
+
options: ModelManagerOptions<Api>;
|
|
1050
|
+
authGeneration: string;
|
|
1051
|
+
apiKey: string | undefined;
|
|
1052
|
+
endpoint: string;
|
|
1053
|
+
}
|
|
1017
1054
|
|
|
1018
1055
|
/**
|
|
1019
1056
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
@@ -1028,6 +1065,21 @@ export class ModelRegistry {
|
|
|
1028
1065
|
#customProviderApiKeys: Map<string, string> = new Map();
|
|
1029
1066
|
#providerWebSearchModes: Map<string, WebSearchMode> = new Map();
|
|
1030
1067
|
#keylessProviders: Set<string> = new Set();
|
|
1068
|
+
#optionalAuthProviders: Set<string> = new Set();
|
|
1069
|
+
#credentiallessAuthFallbackProviders: Map<string, string> = new Map();
|
|
1070
|
+
#providerEvidenceApiKeys: Map<string, string | undefined> = new Map();
|
|
1071
|
+
#providerActivity: ReadonlyMap<string, ProviderActivityEvidence> = new Map();
|
|
1072
|
+
#configuredProviderIds: ReadonlySet<string> = new Set();
|
|
1073
|
+
#configuredDiscoveryProviderIds: ReadonlySet<string> = new Set();
|
|
1074
|
+
#descriptorDiscoveryEvidence = new Map<
|
|
1075
|
+
string,
|
|
1076
|
+
{ fresh: boolean; modelIds: ReadonlySet<string>; authGeneration: string; endpoint: string }
|
|
1077
|
+
>();
|
|
1078
|
+
#descriptorDiscoveryGenerations = new Map<string, number>();
|
|
1079
|
+
#configuredDiscoveryEvidence = new Map<
|
|
1080
|
+
string,
|
|
1081
|
+
{ authGeneration: string; endpoint: string; modelIds: ReadonlySet<string> }
|
|
1082
|
+
>();
|
|
1031
1083
|
#discoveryManager = new ModelDiscoveryManager<DiscoveryProviderConfig>();
|
|
1032
1084
|
#customModelOverlays: CustomModelOverlay[] = [];
|
|
1033
1085
|
#providerOverrides: Map<string, ProviderOverride> = new Map();
|
|
@@ -1038,6 +1090,9 @@ export class ModelRegistry {
|
|
|
1038
1090
|
#configError: ConfigError | undefined = undefined;
|
|
1039
1091
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
1040
1092
|
#lastStaticLoadMtime: number | null = null;
|
|
1093
|
+
#lastStaticLoadEnvironmentFingerprint: string | undefined;
|
|
1094
|
+
#staticModelsLoaded = false;
|
|
1095
|
+
#lastDisabledProviderKey: string | undefined;
|
|
1041
1096
|
#registeredProviderSources: Set<string> = new Set();
|
|
1042
1097
|
#cacheDbPath?: string;
|
|
1043
1098
|
#suppressedSelectors: Map<string, number> = new Map();
|
|
@@ -1051,6 +1106,9 @@ export class ModelRegistry {
|
|
|
1051
1106
|
#runtimeProviderSourceByName: Map<string, string> = new Map();
|
|
1052
1107
|
#rebuildPending: boolean = false;
|
|
1053
1108
|
#rebuildSuspended: number = 0;
|
|
1109
|
+
#configuredApiKeyEnvNames: Set<string> = new Set();
|
|
1110
|
+
#optionalAuthPreflightGenerations = new Map<string, number>();
|
|
1111
|
+
#optionalAuthPreflightEpoch = 0;
|
|
1054
1112
|
|
|
1055
1113
|
/**
|
|
1056
1114
|
* @param authStorage - Auth storage for API key resolution
|
|
@@ -1120,17 +1178,48 @@ export class ModelRegistry {
|
|
|
1120
1178
|
}
|
|
1121
1179
|
}
|
|
1122
1180
|
|
|
1181
|
+
#getStaticLoadEnvironmentFingerprint(): string {
|
|
1182
|
+
const providerBaseUrlEnvKeys = new Set(
|
|
1183
|
+
[
|
|
1184
|
+
...getBundledProviders(),
|
|
1185
|
+
...PROVIDER_DESCRIPTORS.map(descriptor => descriptor.providerId),
|
|
1186
|
+
...this.#configuredProviderIds,
|
|
1187
|
+
].flatMap(getProviderBaseUrlEnvKeys),
|
|
1188
|
+
);
|
|
1189
|
+
return JSON.stringify({
|
|
1190
|
+
apiKeyEnv: [...this.#configuredApiKeyEnvNames].sort().map(name => [name, Bun.env[name] ?? ""]),
|
|
1191
|
+
implicitEndpoints: [
|
|
1192
|
+
["OLLAMA_BASE_URL", Bun.env.OLLAMA_BASE_URL || ""],
|
|
1193
|
+
["LLAMA_CPP_BASE_URL", Bun.env.LLAMA_CPP_BASE_URL || ""],
|
|
1194
|
+
["LM_STUDIO_BASE_URL", Bun.env.LM_STUDIO_BASE_URL || ""],
|
|
1195
|
+
],
|
|
1196
|
+
providerBaseUrls: [...providerBaseUrlEnvKeys].sort().map(name => [name, Bun.env[name] ?? ""]),
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1123
1200
|
#reloadStaticModels(): void {
|
|
1124
1201
|
const currentMtime = this.#modelsConfigFile.getMtimeMs();
|
|
1125
|
-
|
|
1126
|
-
|
|
1202
|
+
const disabledProviderKey = [...getDisabledProviderIdsFromSettings()].sort().join("\u0000");
|
|
1203
|
+
const environmentFingerprint = this.#getStaticLoadEnvironmentFingerprint();
|
|
1204
|
+
if (
|
|
1205
|
+
this.#staticModelsLoaded &&
|
|
1206
|
+
currentMtime === this.#lastStaticLoadMtime &&
|
|
1207
|
+
disabledProviderKey === this.#lastDisabledProviderKey &&
|
|
1208
|
+
environmentFingerprint === this.#lastStaticLoadEnvironmentFingerprint
|
|
1209
|
+
) {
|
|
1210
|
+
// models.json and settings-derived implicit provider state are unchanged.
|
|
1127
1211
|
return;
|
|
1128
1212
|
}
|
|
1129
1213
|
this.#modelsConfigFile.invalidate();
|
|
1130
1214
|
this.#customProviderApiKeys.clear();
|
|
1131
1215
|
this.#providerWebSearchModes.clear();
|
|
1132
1216
|
this.#keylessProviders.clear();
|
|
1217
|
+
this.#optionalAuthProviders.clear();
|
|
1218
|
+
this.#credentiallessAuthFallbackProviders.clear();
|
|
1219
|
+
this.#optionalAuthPreflightEpoch += 1;
|
|
1133
1220
|
this.#discoveryManager.reset();
|
|
1221
|
+
for (const descriptor of PROVIDER_DESCRIPTORS) this.#clearDescriptorDiscoveryEvidence(descriptor.providerId);
|
|
1222
|
+
this.#configuredDiscoveryEvidence.clear();
|
|
1134
1223
|
// Drop config-sourced apiKeys from AuthStorage before reload; entries
|
|
1135
1224
|
// removed from models.yml must actually disappear from the resolver, not
|
|
1136
1225
|
// linger from the previous parse. The post-load setters below repopulate.
|
|
@@ -1146,6 +1235,7 @@ export class ModelRegistry {
|
|
|
1146
1235
|
this.#modelBindingsApplier.setBindings(undefined);
|
|
1147
1236
|
this.#configError = undefined;
|
|
1148
1237
|
this.#loadModels();
|
|
1238
|
+
this.#lastDisabledProviderKey = disabledProviderKey;
|
|
1149
1239
|
}
|
|
1150
1240
|
|
|
1151
1241
|
/**
|
|
@@ -1172,6 +1262,8 @@ export class ModelRegistry {
|
|
|
1172
1262
|
this.#configError = configError;
|
|
1173
1263
|
this.#keylessProviders = keylessProviders;
|
|
1174
1264
|
this.#discoveryManager.setProviders(discoverableProviders);
|
|
1265
|
+
this.#configuredProviderIds = new Set(configuredProviders);
|
|
1266
|
+
this.#configuredDiscoveryProviderIds = new Set(discoverableProviders.map(provider => provider.provider));
|
|
1175
1267
|
this.#customModelOverlays = customModels;
|
|
1176
1268
|
this.#providerOverrides = overrides;
|
|
1177
1269
|
this.#modelOverrides = modelOverrides;
|
|
@@ -1192,10 +1284,57 @@ export class ModelRegistry {
|
|
|
1192
1284
|
const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
|
|
1193
1285
|
const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
1194
1286
|
this.#models = applyFinalCodexGpt56ContextCap(this.#applyRuntimeProviderOverrides(withModelOverrides));
|
|
1287
|
+
this.#rebuildProviderActivity();
|
|
1195
1288
|
this.#rebuildCanonicalIndex();
|
|
1196
1289
|
this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
|
|
1290
|
+
this.#lastStaticLoadEnvironmentFingerprint = this.#getStaticLoadEnvironmentFingerprint();
|
|
1291
|
+
this.#staticModelsLoaded = true;
|
|
1197
1292
|
}
|
|
1198
1293
|
|
|
1294
|
+
#rebuildProviderActivity(): void {
|
|
1295
|
+
const staticModelIds = new Map<string, Set<string>>();
|
|
1296
|
+
const addStaticModel = (provider: string, id: string) => {
|
|
1297
|
+
const modelIds = staticModelIds.get(provider) ?? new Set<string>();
|
|
1298
|
+
modelIds.add(id);
|
|
1299
|
+
staticModelIds.set(provider, modelIds);
|
|
1300
|
+
};
|
|
1301
|
+
for (const provider of getBundledProviders()) {
|
|
1302
|
+
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[])
|
|
1303
|
+
addStaticModel(provider, model.id);
|
|
1304
|
+
}
|
|
1305
|
+
for (const overlay of [...this.#customModelOverlays, ...this.#runtimeModelOverlays])
|
|
1306
|
+
addStaticModel(overlay.provider, overlay.id);
|
|
1307
|
+
|
|
1308
|
+
const runtimeProviderIds = new Set(this.#runtimeProviderSourceByName.keys());
|
|
1309
|
+
const providerIds = new Set<string>([
|
|
1310
|
+
...this.#configuredProviderIds,
|
|
1311
|
+
...this.#keylessProviders,
|
|
1312
|
+
...this.#discoveryManager.providerIds(),
|
|
1313
|
+
...this.#descriptorDiscoveryEvidence.keys(),
|
|
1314
|
+
...runtimeProviderIds,
|
|
1315
|
+
...staticModelIds.keys(),
|
|
1316
|
+
]);
|
|
1317
|
+
const activity = new Map<string, ProviderActivityEvidence>();
|
|
1318
|
+
for (const provider of providerIds) {
|
|
1319
|
+
const discoveryConfigured = this.#configuredDiscoveryProviderIds.has(provider);
|
|
1320
|
+
const isDiscoveryProvider = this.#discoveryManager.providerIds().has(provider);
|
|
1321
|
+
const descriptorEvidence = this.#descriptorDiscoveryEvidence.get(provider);
|
|
1322
|
+
activity.set(provider, {
|
|
1323
|
+
staticModelIds: new Set(staticModelIds.get(provider) ?? []),
|
|
1324
|
+
staticConfigured: staticModelIds.has(provider),
|
|
1325
|
+
discoveryConfigured,
|
|
1326
|
+
implicitDiscovery: isDiscoveryProvider && !discoveryConfigured,
|
|
1327
|
+
descriptorBacked:
|
|
1328
|
+
descriptorEvidence !== undefined ||
|
|
1329
|
+
(!discoveryConfigured && PROVIDER_DESCRIPTORS.some(descriptor => descriptor.providerId === provider)),
|
|
1330
|
+
descriptorFresh: descriptorEvidence?.fresh ?? false,
|
|
1331
|
+
descriptorModelIds: new Set(descriptorEvidence?.modelIds ?? []),
|
|
1332
|
+
authGeneration: descriptorEvidence?.authGeneration ?? "",
|
|
1333
|
+
endpoint: descriptorEvidence?.endpoint ?? "",
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
this.#providerActivity = activity;
|
|
1337
|
+
}
|
|
1199
1338
|
/** Load built-in models, applying provider-level overrides only.
|
|
1200
1339
|
* Per-model overrides are applied later by #applyModelOverrides. */
|
|
1201
1340
|
#loadBuiltInModels(overrides: Map<string, ProviderOverride>): Model<Api>[] {
|
|
@@ -1338,6 +1477,12 @@ export class ModelRegistry {
|
|
|
1338
1477
|
}
|
|
1339
1478
|
|
|
1340
1479
|
#normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
1480
|
+
const liveBaseUrl =
|
|
1481
|
+
providerConfig.discovery.type === "openai-models-list" || providerConfig.discovery.type === "lm-studio"
|
|
1482
|
+
? this.#normalizeOpenAIModelsListBaseUrl(
|
|
1483
|
+
this.#getProviderBaseUrlForDiscovery(providerConfig.provider) ?? providerConfig.baseUrl,
|
|
1484
|
+
)
|
|
1485
|
+
: undefined;
|
|
1341
1486
|
return models.map(model => {
|
|
1342
1487
|
const normalized =
|
|
1343
1488
|
providerConfig.provider === "ollama" &&
|
|
@@ -1345,8 +1490,10 @@ export class ModelRegistry {
|
|
|
1345
1490
|
model.api === "openai-completions"
|
|
1346
1491
|
? ({ ...model, api: "openai-responses" } as Model<Api>)
|
|
1347
1492
|
: model;
|
|
1493
|
+
const baseUrl = this.#restoreLiveDiscoveryBaseUrl(normalized.baseUrl, liveBaseUrl);
|
|
1348
1494
|
return {
|
|
1349
1495
|
...normalized,
|
|
1496
|
+
...(baseUrl !== normalized.baseUrl ? { baseUrl } : {}),
|
|
1350
1497
|
requestTransform: providerConfig.requestTransform
|
|
1351
1498
|
? mergeRequestTransform(undefined, providerConfig.requestTransform)
|
|
1352
1499
|
: undefined,
|
|
@@ -1354,6 +1501,21 @@ export class ModelRegistry {
|
|
|
1354
1501
|
};
|
|
1355
1502
|
});
|
|
1356
1503
|
}
|
|
1504
|
+
#sanitizeDiscoverableModelsForCache(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
1505
|
+
return providerConfig.discovery.type === "openai-models-list" || providerConfig.discovery.type === "lm-studio"
|
|
1506
|
+
? this.#stripModelBaseUrlQueries(models)
|
|
1507
|
+
: models;
|
|
1508
|
+
}
|
|
1509
|
+
#stripModelBaseUrlQueries(models: readonly Model<Api>[]): Model<Api>[] {
|
|
1510
|
+
return models.map(model => (model.baseUrl ? { ...model, baseUrl: stripUrlQuery(model.baseUrl) } : model));
|
|
1511
|
+
}
|
|
1512
|
+
#restoreLiveDiscoveryBaseUrl(modelBaseUrl: string | undefined, liveBaseUrl: string | undefined): string | undefined {
|
|
1513
|
+
if (!modelBaseUrl || !liveBaseUrl?.includes("?")) return modelBaseUrl;
|
|
1514
|
+
return this.#normalizeDiscoveryEvidenceEndpoint(stripUrlQuery(modelBaseUrl)) ===
|
|
1515
|
+
this.#normalizeDiscoveryEvidenceEndpoint(stripUrlQuery(liveBaseUrl))
|
|
1516
|
+
? liveBaseUrl
|
|
1517
|
+
: modelBaseUrl;
|
|
1518
|
+
}
|
|
1357
1519
|
|
|
1358
1520
|
#addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
|
|
1359
1521
|
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
@@ -1365,6 +1527,8 @@ export class ModelRegistry {
|
|
|
1365
1527
|
discovery: { type: "ollama" },
|
|
1366
1528
|
optional: true,
|
|
1367
1529
|
});
|
|
1530
|
+
// Implicit Ollama auth is optional and may be added after startup.
|
|
1531
|
+
this.#optionalAuthProviders.add("ollama");
|
|
1368
1532
|
this.#keylessProviders.add("ollama");
|
|
1369
1533
|
}
|
|
1370
1534
|
if (!configuredProviders.has("llama.cpp") && !disabledProviders.has("llama.cpp")) {
|
|
@@ -1375,10 +1539,9 @@ export class ModelRegistry {
|
|
|
1375
1539
|
discovery: { type: "llama.cpp" },
|
|
1376
1540
|
optional: true,
|
|
1377
1541
|
});
|
|
1378
|
-
//
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1542
|
+
// Implicit llama.cpp auth is optional and may be added after startup.
|
|
1543
|
+
this.#optionalAuthProviders.add("llama.cpp");
|
|
1544
|
+
this.#keylessProviders.add("llama.cpp");
|
|
1382
1545
|
}
|
|
1383
1546
|
if (!configuredProviders.has("lm-studio") && !disabledProviders.has("lm-studio")) {
|
|
1384
1547
|
this.#discoveryManager.addProvider({
|
|
@@ -1388,11 +1551,14 @@ export class ModelRegistry {
|
|
|
1388
1551
|
discovery: { type: "lm-studio" },
|
|
1389
1552
|
optional: true,
|
|
1390
1553
|
});
|
|
1554
|
+
// Implicit LM Studio auth is optional and may be added after startup.
|
|
1555
|
+
this.#optionalAuthProviders.add("lm-studio");
|
|
1391
1556
|
this.#keylessProviders.add("lm-studio");
|
|
1392
1557
|
}
|
|
1393
1558
|
}
|
|
1394
1559
|
|
|
1395
1560
|
#loadCustomModels(): CustomModelsResult {
|
|
1561
|
+
this.#configuredApiKeyEnvNames.clear();
|
|
1396
1562
|
const { value, error, status } = this.#modelsConfigFile.tryLoad();
|
|
1397
1563
|
|
|
1398
1564
|
if (status === "error") {
|
|
@@ -1428,6 +1594,12 @@ export class ModelRegistry {
|
|
|
1428
1594
|
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
1429
1595
|
|
|
1430
1596
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
1597
|
+
if (providerConfig.apiKeyEnv) this.#configuredApiKeyEnvNames.add(providerConfig.apiKeyEnv);
|
|
1598
|
+
if (providerConfig.apiKey) this.#configuredApiKeyEnvNames.add(providerConfig.apiKey);
|
|
1599
|
+
if (providerConfig.openaiCompat?.apiKeyEnv)
|
|
1600
|
+
this.#configuredApiKeyEnvNames.add(providerConfig.openaiCompat.apiKeyEnv);
|
|
1601
|
+
if (providerConfig.openaiCompat?.apiKey)
|
|
1602
|
+
this.#configuredApiKeyEnvNames.add(providerConfig.openaiCompat.apiKey);
|
|
1431
1603
|
if (providerConfig.webSearch) this.#providerWebSearchModes.set(providerName, providerConfig.webSearch);
|
|
1432
1604
|
const providerApiKeyConfig = providerConfig.apiKey ?? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
|
|
1433
1605
|
const localOpenAICompat = providerConfig.openaiCompat;
|
|
@@ -1467,6 +1639,7 @@ export class ModelRegistry {
|
|
|
1467
1639
|
this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey);
|
|
1468
1640
|
} else {
|
|
1469
1641
|
keylessProviders.add(providerName);
|
|
1642
|
+
this.#optionalAuthProviders.add(providerName);
|
|
1470
1643
|
}
|
|
1471
1644
|
}
|
|
1472
1645
|
// Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
|
|
@@ -1701,15 +1874,145 @@ export class ModelRegistry {
|
|
|
1701
1874
|
).filter(provider => !disabledProviders.has(provider.provider));
|
|
1702
1875
|
const configuredDiscoveriesPromise =
|
|
1703
1876
|
selectedDiscoverableProviders.length === 0
|
|
1704
|
-
? Promise.resolve
|
|
1877
|
+
? Promise.resolve(
|
|
1878
|
+
[] as Array<{
|
|
1879
|
+
provider: string;
|
|
1880
|
+
current: boolean;
|
|
1881
|
+
models: Model<Api>[];
|
|
1882
|
+
authGeneration: string;
|
|
1883
|
+
configurationGeneration: number;
|
|
1884
|
+
endpoint: string;
|
|
1885
|
+
fetched: boolean;
|
|
1886
|
+
}>,
|
|
1887
|
+
)
|
|
1705
1888
|
: Promise.all(
|
|
1706
1889
|
selectedDiscoverableProviders.map(provider => this.#discoverProviderModels(provider, strategy)),
|
|
1707
|
-
)
|
|
1708
|
-
const [
|
|
1890
|
+
);
|
|
1891
|
+
const [configuredDiscoveryResults, builtInDiscovered] = await Promise.all([
|
|
1709
1892
|
configuredDiscoveriesPromise,
|
|
1710
1893
|
this.#discoverBuiltInProviderModels(strategy, providerFilter),
|
|
1711
1894
|
]);
|
|
1712
|
-
const
|
|
1895
|
+
const currentConfiguredDiscoveryResults = configuredDiscoveryResults.map(result => {
|
|
1896
|
+
const providerConfig = selectedDiscoverableProviders.find(provider => provider.provider === result.provider);
|
|
1897
|
+
const current =
|
|
1898
|
+
result.current &&
|
|
1899
|
+
providerConfig !== undefined &&
|
|
1900
|
+
(() => {
|
|
1901
|
+
try {
|
|
1902
|
+
return (
|
|
1903
|
+
result.authGeneration ===
|
|
1904
|
+
this.#getProviderEvidenceGeneration(
|
|
1905
|
+
result.provider,
|
|
1906
|
+
this.#providerEvidenceApiKeys.get(result.provider),
|
|
1907
|
+
) &&
|
|
1908
|
+
result.endpoint ===
|
|
1909
|
+
this.#normalizeDiscoveryEvidenceEndpoint(
|
|
1910
|
+
this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "",
|
|
1911
|
+
)
|
|
1912
|
+
);
|
|
1913
|
+
} catch {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
})();
|
|
1917
|
+
const invalidatesPublishedState =
|
|
1918
|
+
result.current &&
|
|
1919
|
+
providerConfig !== undefined &&
|
|
1920
|
+
(() => {
|
|
1921
|
+
try {
|
|
1922
|
+
return (
|
|
1923
|
+
result.authGeneration !==
|
|
1924
|
+
this.#getProviderEvidenceGeneration(
|
|
1925
|
+
result.provider,
|
|
1926
|
+
this.#providerEvidenceApiKeys.get(result.provider),
|
|
1927
|
+
) ||
|
|
1928
|
+
result.configurationGeneration !==
|
|
1929
|
+
this.authStorage.getProviderConfigurationGeneration(result.provider) ||
|
|
1930
|
+
result.endpoint !==
|
|
1931
|
+
this.#normalizeDiscoveryEvidenceEndpoint(
|
|
1932
|
+
this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "",
|
|
1933
|
+
)
|
|
1934
|
+
);
|
|
1935
|
+
} catch {
|
|
1936
|
+
return true;
|
|
1937
|
+
}
|
|
1938
|
+
})();
|
|
1939
|
+
return current
|
|
1940
|
+
? { ...result, invalidatesPublishedState }
|
|
1941
|
+
: { ...result, current: false, models: [], fetched: false, invalidatesPublishedState };
|
|
1942
|
+
});
|
|
1943
|
+
const currentBuiltInDiscovered = builtInDiscovered.filter(model => {
|
|
1944
|
+
const evidence = this.#descriptorDiscoveryEvidence.get(model.provider);
|
|
1945
|
+
const currentEndpoint = this.#normalizeDiscoveryEvidenceEndpoint(
|
|
1946
|
+
this.#getProviderBaseUrlForDiscovery(model.provider) ?? model.baseUrl ?? "",
|
|
1947
|
+
);
|
|
1948
|
+
const canUseCredentialDerivedXiaomiEndpoint =
|
|
1949
|
+
model.provider === "xiaomi" &&
|
|
1950
|
+
this.#providerEvidenceApiKeys.get("xiaomi")?.startsWith("tp-") === true &&
|
|
1951
|
+
this.#runtimeProviderOverrides.get("xiaomi")?.baseUrl === undefined &&
|
|
1952
|
+
this.#providerOverrides.get("xiaomi")?.baseUrl === undefined &&
|
|
1953
|
+
resolveProviderBaseUrlFromEnv("xiaomi") === undefined;
|
|
1954
|
+
try {
|
|
1955
|
+
return (
|
|
1956
|
+
evidence !== undefined &&
|
|
1957
|
+
evidence.authGeneration ===
|
|
1958
|
+
this.#getProviderEvidenceGeneration(
|
|
1959
|
+
model.provider,
|
|
1960
|
+
this.#providerEvidenceApiKeys.get(model.provider),
|
|
1961
|
+
) &&
|
|
1962
|
+
(evidence.endpoint === currentEndpoint ||
|
|
1963
|
+
(canUseCredentialDerivedXiaomiEndpoint &&
|
|
1964
|
+
evidence.endpoint === this.#normalizeDiscoveryEvidenceEndpoint(model.baseUrl ?? ""))) &&
|
|
1965
|
+
evidence.modelIds.has(model.id)
|
|
1966
|
+
);
|
|
1967
|
+
} catch {
|
|
1968
|
+
return false;
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
const configuredDiscoveryEvidence = new Map(
|
|
1972
|
+
currentConfiguredDiscoveryResults
|
|
1973
|
+
.filter(result => result.current)
|
|
1974
|
+
.map(result => [
|
|
1975
|
+
result.provider,
|
|
1976
|
+
{
|
|
1977
|
+
authGeneration: result.authGeneration,
|
|
1978
|
+
endpoint: result.endpoint,
|
|
1979
|
+
modelIds: new Set(result.models.map(model => model.id)),
|
|
1980
|
+
},
|
|
1981
|
+
]),
|
|
1982
|
+
);
|
|
1983
|
+
const configuredDiscoveries = new Map(currentConfiguredDiscoveryResults.map(result => [result.provider, result]));
|
|
1984
|
+
const configuredDiscovered = currentConfiguredDiscoveryResults.flatMap(result => result.models);
|
|
1985
|
+
const discovered = [...configuredDiscovered, ...currentBuiltInDiscovered];
|
|
1986
|
+
for (const provider of selectedDiscoverableProviders) {
|
|
1987
|
+
const evidence = configuredDiscoveryEvidence.get(provider.provider);
|
|
1988
|
+
const discovery = configuredDiscoveries.get(provider.provider);
|
|
1989
|
+
const state = this.#discoveryManager.getState(provider.provider);
|
|
1990
|
+
if (!discovery?.current) {
|
|
1991
|
+
if (discovery?.invalidatesPublishedState) this.#discoveryManager.invalidate(provider.provider);
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const currentAuthGeneration = discovery.authGeneration;
|
|
1995
|
+
const currentEndpoint = this.#normalizeDiscoveryEvidenceEndpoint(
|
|
1996
|
+
this.#effectiveDiscoveryProviderConfig(provider).baseUrl ?? "",
|
|
1997
|
+
);
|
|
1998
|
+
if (
|
|
1999
|
+
evidence !== undefined &&
|
|
2000
|
+
state?.status === "ok" &&
|
|
2001
|
+
discovery.fetched &&
|
|
2002
|
+
currentAuthGeneration === evidence.authGeneration &&
|
|
2003
|
+
currentEndpoint === evidence.endpoint
|
|
2004
|
+
) {
|
|
2005
|
+
this.#configuredDiscoveryEvidence.set(provider.provider, evidence);
|
|
2006
|
+
} else if (
|
|
2007
|
+
(state?.status !== "cached" && !(state?.status === "ok" && !discovery.fetched)) ||
|
|
2008
|
+
state.error !== undefined ||
|
|
2009
|
+
this.#configuredDiscoveryEvidence.get(provider.provider)?.authGeneration !== currentAuthGeneration ||
|
|
2010
|
+
this.#configuredDiscoveryEvidence.get(provider.provider)?.endpoint !== currentEndpoint
|
|
2011
|
+
) {
|
|
2012
|
+
this.#configuredDiscoveryEvidence.delete(provider.provider);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
this.#rebuildProviderActivity();
|
|
1713
2016
|
if (discovered.length === 0) {
|
|
1714
2017
|
return;
|
|
1715
2018
|
}
|
|
@@ -1734,40 +2037,198 @@ export class ModelRegistry {
|
|
|
1734
2037
|
async #discoverProviderModels(
|
|
1735
2038
|
providerConfig: DiscoveryProviderConfig,
|
|
1736
2039
|
strategy: ModelRefreshStrategy,
|
|
1737
|
-
): Promise<
|
|
1738
|
-
|
|
2040
|
+
): Promise<{
|
|
2041
|
+
provider: string;
|
|
2042
|
+
current: boolean;
|
|
2043
|
+
models: Model<Api>[];
|
|
2044
|
+
authGeneration: string;
|
|
2045
|
+
configurationGeneration: number;
|
|
2046
|
+
endpoint: string;
|
|
2047
|
+
fetched: boolean;
|
|
2048
|
+
}> {
|
|
2049
|
+
const provider = providerConfig.provider;
|
|
2050
|
+
const preflightEpoch = this.#optionalAuthPreflightEpoch;
|
|
2051
|
+
const preflightGeneration = (this.#optionalAuthPreflightGenerations.get(provider) ?? 0) + 1;
|
|
2052
|
+
this.#optionalAuthPreflightGenerations.set(provider, preflightGeneration);
|
|
2053
|
+
const isCurrentPreflight = () =>
|
|
2054
|
+
this.#optionalAuthPreflightEpoch === preflightEpoch &&
|
|
2055
|
+
this.#optionalAuthPreflightGenerations.get(provider) === preflightGeneration;
|
|
2056
|
+
let preflightApiKey: string | undefined;
|
|
2057
|
+
let preflightFailed = false;
|
|
2058
|
+
let preflightStale = false;
|
|
2059
|
+
let preflightCompleted = false;
|
|
2060
|
+
const optionalAuth = this.#optionalAuthProviders.has(provider);
|
|
2061
|
+
const shouldPreflightAuth = optionalAuth
|
|
2062
|
+
? this.authStorage.has(provider) || this.authStorage.hasAuth(provider)
|
|
2063
|
+
: !this.#isCredentiallessProvider(provider);
|
|
2064
|
+
let preflightAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(provider);
|
|
2065
|
+
let preflightOAuthRefreshGeneration = this.authStorage.getProviderOAuthRefreshGeneration(provider);
|
|
2066
|
+
if (shouldPreflightAuth) {
|
|
2067
|
+
if (optionalAuth && isCurrentPreflight()) this.#credentiallessAuthFallbackProviders.delete(provider);
|
|
2068
|
+
let apiKey: string | undefined;
|
|
2069
|
+
try {
|
|
2070
|
+
apiKey = await this.#peekApiKeyForProvider(provider, {
|
|
2071
|
+
ignoreCredentiallessFallback: optionalAuth,
|
|
2072
|
+
refreshOAuth: true,
|
|
2073
|
+
baseUrl: providerConfig.baseUrl,
|
|
2074
|
+
});
|
|
2075
|
+
const currentAuthConfigurationGeneration = this.authStorage.getProviderConfigurationGeneration(provider);
|
|
2076
|
+
if (preflightAuthConfigurationGeneration !== currentAuthConfigurationGeneration) {
|
|
2077
|
+
const currentOAuthRefreshGeneration = this.authStorage.getProviderOAuthRefreshGeneration(provider);
|
|
2078
|
+
if (
|
|
2079
|
+
currentOAuthRefreshGeneration === preflightOAuthRefreshGeneration ||
|
|
2080
|
+
currentAuthConfigurationGeneration - preflightAuthConfigurationGeneration !==
|
|
2081
|
+
currentOAuthRefreshGeneration - preflightOAuthRefreshGeneration
|
|
2082
|
+
) {
|
|
2083
|
+
preflightStale = true;
|
|
2084
|
+
} else {
|
|
2085
|
+
preflightAuthConfigurationGeneration = currentAuthConfigurationGeneration;
|
|
2086
|
+
preflightOAuthRefreshGeneration = currentOAuthRefreshGeneration;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
preflightFailed = true;
|
|
2091
|
+
logger.warn("model discovery credential preflight failed", {
|
|
2092
|
+
provider,
|
|
2093
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
preflightApiKey = apiKey;
|
|
2097
|
+
preflightCompleted = true;
|
|
2098
|
+
if (!preflightFailed && optionalAuth && isCurrentPreflight()) {
|
|
2099
|
+
this.#providerEvidenceApiKeys.set(provider, apiKey);
|
|
2100
|
+
const authGeneration = this.authStorage.getProviderEvidenceGeneration(provider, apiKey);
|
|
2101
|
+
if (apiKey === undefined) this.#credentiallessAuthFallbackProviders.set(provider, authGeneration);
|
|
2102
|
+
else this.#credentiallessAuthFallbackProviders.delete(provider);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
const effectiveProviderConfig = this.#effectiveDiscoveryProviderConfig(providerConfig);
|
|
2106
|
+
const endpoint = this.#normalizeDiscoveryEvidenceEndpoint(effectiveProviderConfig.baseUrl ?? "");
|
|
2107
|
+
if (!isCurrentPreflight() || preflightStale) {
|
|
2108
|
+
return {
|
|
2109
|
+
provider: effectiveProviderConfig.provider,
|
|
2110
|
+
current: false,
|
|
2111
|
+
models: [],
|
|
2112
|
+
authGeneration: this.#getProviderEvidenceGeneration(provider, preflightApiKey),
|
|
2113
|
+
configurationGeneration: preflightAuthConfigurationGeneration,
|
|
2114
|
+
endpoint,
|
|
2115
|
+
fetched: false,
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
if (optionalAuth && preflightFailed) {
|
|
2119
|
+
return {
|
|
2120
|
+
provider: effectiveProviderConfig.provider,
|
|
2121
|
+
current: false,
|
|
2122
|
+
models: [],
|
|
2123
|
+
authGeneration: this.#getProviderEvidenceGeneration(provider, preflightApiKey),
|
|
2124
|
+
configurationGeneration: preflightAuthConfigurationGeneration,
|
|
2125
|
+
endpoint,
|
|
2126
|
+
fetched: false,
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
const authGenerationBeforeDiscovery = this.#getProviderEvidenceGeneration(provider, preflightApiKey);
|
|
2130
|
+
const isCurrentEndpoint = () =>
|
|
2131
|
+
endpoint ===
|
|
2132
|
+
this.#normalizeDiscoveryEvidenceEndpoint(this.#effectiveDiscoveryProviderConfig(providerConfig).baseUrl ?? "");
|
|
2133
|
+
const evidence = this.#configuredDiscoveryEvidence.get(provider);
|
|
2134
|
+
const refreshStrategy =
|
|
2135
|
+
strategy === "online-if-uncached" &&
|
|
2136
|
+
evidence !== undefined &&
|
|
2137
|
+
(evidence.authGeneration !== authGenerationBeforeDiscovery || evidence.endpoint !== endpoint)
|
|
2138
|
+
? "online"
|
|
2139
|
+
: strategy;
|
|
2140
|
+
const mergeInput = await this.#discoveryManager.discover(effectiveProviderConfig, refreshStrategy, {
|
|
1739
2141
|
cacheDbPath: this.#cacheDbPath,
|
|
1740
|
-
requiresAuth: provider => !this.#
|
|
1741
|
-
peekApiKey: provider =>
|
|
2142
|
+
requiresAuth: provider => !this.#isCredentiallessProvider(provider.provider),
|
|
2143
|
+
peekApiKey: async provider =>
|
|
2144
|
+
preflightCompleted
|
|
2145
|
+
? preflightApiKey
|
|
2146
|
+
: this.#peekApiKeyForProvider(provider.provider, {
|
|
2147
|
+
refreshOAuth: true,
|
|
2148
|
+
baseUrl: provider.baseUrl,
|
|
2149
|
+
}),
|
|
1742
2150
|
isAuthenticated,
|
|
1743
|
-
fetchModels:
|
|
2151
|
+
fetchModels: async (provider, apiKey) =>
|
|
2152
|
+
this.#sanitizeDiscoverableModelsForCache(
|
|
2153
|
+
provider,
|
|
2154
|
+
await this.#discoverModelsByProviderType(provider, apiKey),
|
|
2155
|
+
),
|
|
2156
|
+
getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
|
|
2157
|
+
canPublishCache: () => isCurrentEndpoint(),
|
|
1744
2158
|
});
|
|
1745
|
-
|
|
2159
|
+
const authGeneration =
|
|
2160
|
+
mergeInput.authGeneration ??
|
|
2161
|
+
this.#getProviderEvidenceGeneration(effectiveProviderConfig.provider, preflightApiKey);
|
|
2162
|
+
const current =
|
|
2163
|
+
mergeInput.current &&
|
|
2164
|
+
authGeneration === this.#getProviderEvidenceGeneration(effectiveProviderConfig.provider, preflightApiKey) &&
|
|
2165
|
+
isCurrentEndpoint();
|
|
2166
|
+
if (!current) {
|
|
2167
|
+
return {
|
|
2168
|
+
provider: effectiveProviderConfig.provider,
|
|
2169
|
+
current: false,
|
|
2170
|
+
models: [],
|
|
2171
|
+
authGeneration,
|
|
2172
|
+
configurationGeneration: preflightAuthConfigurationGeneration,
|
|
2173
|
+
endpoint,
|
|
2174
|
+
fetched: false,
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
1746
2177
|
if (mergeInput.warning) {
|
|
1747
2178
|
logger.warn("model discovery failed for provider", {
|
|
1748
|
-
provider:
|
|
1749
|
-
url:
|
|
2179
|
+
provider: effectiveProviderConfig.provider,
|
|
2180
|
+
url: redactDiscoveryUrl(effectiveProviderConfig.baseUrl ?? ""),
|
|
1750
2181
|
error: mergeInput.warning,
|
|
1751
2182
|
});
|
|
1752
2183
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
2184
|
+
this.#providerEvidenceApiKeys.set(effectiveProviderConfig.provider, preflightApiKey);
|
|
2185
|
+
return {
|
|
2186
|
+
provider: effectiveProviderConfig.provider,
|
|
2187
|
+
current: true,
|
|
2188
|
+
authGeneration,
|
|
2189
|
+
configurationGeneration: preflightAuthConfigurationGeneration,
|
|
2190
|
+
endpoint,
|
|
2191
|
+
fetched: mergeInput.fetched ?? false,
|
|
2192
|
+
models: this.#applyProviderModelOverrides(
|
|
2193
|
+
effectiveProviderConfig.provider,
|
|
2194
|
+
this.#normalizeDiscoverableModels(
|
|
2195
|
+
effectiveProviderConfig,
|
|
2196
|
+
this.#applyProviderCompat(effectiveProviderConfig.compat, [...mergeInput.models]),
|
|
2197
|
+
),
|
|
1758
2198
|
),
|
|
1759
|
-
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
#effectiveDiscoveryProviderConfig(providerConfig: DiscoveryProviderConfig): DiscoveryProviderConfig {
|
|
2202
|
+
const override = this.#runtimeProviderOverrides.get(providerConfig.provider);
|
|
2203
|
+
const baseUrl = this.#getProviderBaseUrlForDiscovery(providerConfig.provider) ?? providerConfig.baseUrl;
|
|
2204
|
+
const effectiveBaseUrl =
|
|
2205
|
+
providerConfig.discovery.type === "ollama"
|
|
2206
|
+
? this.#normalizeOllamaBaseUrl(baseUrl)
|
|
2207
|
+
: providerConfig.discovery.type === "llama.cpp"
|
|
2208
|
+
? this.#normalizeLlamaCppBaseUrl(baseUrl)
|
|
2209
|
+
: this.#normalizeOpenAIModelsListBaseUrl(baseUrl);
|
|
2210
|
+
return {
|
|
2211
|
+
...providerConfig,
|
|
2212
|
+
baseUrl: effectiveBaseUrl,
|
|
2213
|
+
headers: override?.headers ? { ...providerConfig.headers, ...override.headers } : providerConfig.headers,
|
|
2214
|
+
compat: override?.compat ? mergeCompat(providerConfig.compat, override.compat) : providerConfig.compat,
|
|
2215
|
+
requestTransform: mergeRequestTransform(providerConfig.requestTransform, override?.requestTransform),
|
|
2216
|
+
cacheRetention: override?.cacheRetention ?? providerConfig.cacheRetention,
|
|
2217
|
+
};
|
|
1760
2218
|
}
|
|
1761
2219
|
|
|
1762
|
-
#discoverModelsByProviderType(
|
|
2220
|
+
#discoverModelsByProviderType(
|
|
2221
|
+
providerConfig: DiscoveryProviderConfig,
|
|
2222
|
+
apiKey: string | undefined,
|
|
2223
|
+
): Promise<Model<Api>[]> {
|
|
1763
2224
|
switch (providerConfig.discovery.type) {
|
|
1764
2225
|
case "ollama":
|
|
1765
|
-
return this.#discoverOllamaModels(providerConfig);
|
|
2226
|
+
return this.#discoverOllamaModels(providerConfig, apiKey);
|
|
1766
2227
|
case "llama.cpp":
|
|
1767
|
-
return this.#discoverLlamaCppModels(providerConfig);
|
|
2228
|
+
return this.#discoverLlamaCppModels(providerConfig, apiKey);
|
|
1768
2229
|
case "lm-studio":
|
|
1769
2230
|
case "openai-models-list":
|
|
1770
|
-
return this.#discoverOpenAIModelsList(providerConfig);
|
|
2231
|
+
return this.#discoverOpenAIModelsList(providerConfig, apiKey);
|
|
1771
2232
|
}
|
|
1772
2233
|
}
|
|
1773
2234
|
|
|
@@ -1777,22 +2238,21 @@ export class ModelRegistry {
|
|
|
1777
2238
|
): Promise<Model<Api>[]> {
|
|
1778
2239
|
// Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
|
|
1779
2240
|
const configuredDiscoveryProviders = new Set(this.#discoveryManager.providers.map(p => p.provider));
|
|
1780
|
-
const managerOptions = (await this.#collectBuiltInModelManagerOptions()).filter(
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
return providerFilter ? providerFilter.has(opts.providerId) : true;
|
|
1785
|
-
});
|
|
2241
|
+
const managerOptions = (await this.#collectBuiltInModelManagerOptions(configuredDiscoveryProviders)).filter(
|
|
2242
|
+
entry => (providerFilter ? providerFilter.has(entry.options.providerId) : true),
|
|
2243
|
+
);
|
|
1786
2244
|
if (managerOptions.length === 0) {
|
|
1787
2245
|
return [];
|
|
1788
2246
|
}
|
|
1789
2247
|
const discoveries = await Promise.all(
|
|
1790
|
-
managerOptions.map(
|
|
2248
|
+
managerOptions.map(entry => this.#discoverWithModelManager(entry, strategy)),
|
|
1791
2249
|
);
|
|
1792
2250
|
return discoveries.flat();
|
|
1793
2251
|
}
|
|
1794
2252
|
|
|
1795
|
-
async #collectBuiltInModelManagerOptions(
|
|
2253
|
+
async #collectBuiltInModelManagerOptions(
|
|
2254
|
+
excludedProviderIds: ReadonlySet<string> = new Set(),
|
|
2255
|
+
): Promise<ModelManagerDiscoveryOptions[]> {
|
|
1796
2256
|
const specialProviderDescriptors: Array<{
|
|
1797
2257
|
providerId: string;
|
|
1798
2258
|
resolveKey: (value: string | undefined) => string | undefined;
|
|
@@ -1830,55 +2290,180 @@ export class ModelRegistry {
|
|
|
1830
2290
|
];
|
|
1831
2291
|
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
1832
2292
|
const standardProviderDescriptors = PROVIDER_DESCRIPTORS.filter(
|
|
1833
|
-
descriptor => !disabledProviders.has(descriptor.providerId),
|
|
2293
|
+
descriptor => !disabledProviders.has(descriptor.providerId) && !excludedProviderIds.has(descriptor.providerId),
|
|
1834
2294
|
);
|
|
1835
2295
|
const enabledSpecialProviderDescriptors = specialProviderDescriptors.filter(
|
|
1836
|
-
descriptor => !disabledProviders.has(descriptor.providerId),
|
|
2296
|
+
descriptor => !disabledProviders.has(descriptor.providerId) && !excludedProviderIds.has(descriptor.providerId),
|
|
1837
2297
|
);
|
|
2298
|
+
for (const descriptor of standardProviderDescriptors) {
|
|
2299
|
+
if (!descriptor.allowUnauthenticated) continue;
|
|
2300
|
+
this.#keylessProviders.add(descriptor.providerId);
|
|
2301
|
+
this.#optionalAuthProviders.add(descriptor.providerId);
|
|
2302
|
+
}
|
|
1838
2303
|
// Use peekApiKey to avoid OAuth token refresh during discovery.
|
|
1839
2304
|
// The token is only needed if the dynamic fetch fires (cache miss),
|
|
1840
2305
|
// and failures there are handled gracefully.
|
|
1841
|
-
const peekKey = (descriptor: { providerId: string }) =>
|
|
1842
|
-
|
|
2306
|
+
const peekKey = async (descriptor: { providerId: string }) => {
|
|
2307
|
+
const configurationGeneration = this.authStorage.getProviderConfigurationGeneration(descriptor.providerId);
|
|
2308
|
+
const apiKey = await this.#peekApiKeyForProvider(descriptor.providerId);
|
|
2309
|
+
if (configurationGeneration !== this.authStorage.getProviderConfigurationGeneration(descriptor.providerId)) {
|
|
2310
|
+
return { apiKey: undefined, authGeneration: undefined };
|
|
2311
|
+
}
|
|
2312
|
+
return {
|
|
2313
|
+
apiKey,
|
|
2314
|
+
authGeneration: this.#getProviderEvidenceGeneration(descriptor.providerId, apiKey),
|
|
2315
|
+
};
|
|
2316
|
+
};
|
|
2317
|
+
const [standardProviderCredentials, specialProviderCredentials] = await Promise.all([
|
|
1843
2318
|
Promise.all(standardProviderDescriptors.map(peekKey)),
|
|
1844
2319
|
Promise.all(enabledSpecialProviderDescriptors.map(peekKey)),
|
|
1845
2320
|
]);
|
|
1846
|
-
const options:
|
|
2321
|
+
const options: ModelManagerDiscoveryOptions[] = [];
|
|
1847
2322
|
for (let i = 0; i < standardProviderDescriptors.length; i++) {
|
|
1848
2323
|
const descriptor = standardProviderDescriptors[i];
|
|
1849
|
-
const apiKey =
|
|
1850
|
-
if (
|
|
1851
|
-
|
|
1852
|
-
|
|
2324
|
+
const { apiKey, authGeneration } = standardProviderCredentials[i];
|
|
2325
|
+
if (
|
|
2326
|
+
authGeneration !== undefined &&
|
|
2327
|
+
authGeneration === this.#getProviderEvidenceGeneration(descriptor.providerId, apiKey) &&
|
|
2328
|
+
(isAuthenticated(apiKey) || descriptor.allowUnauthenticated)
|
|
2329
|
+
) {
|
|
2330
|
+
const baseUrl = this.#getProviderBaseUrlForDiscovery(descriptor.providerId);
|
|
2331
|
+
options.push({
|
|
2332
|
+
options: descriptor.createModelManagerOptions({
|
|
1853
2333
|
apiKey: isAuthenticated(apiKey) ? apiKey : undefined,
|
|
1854
|
-
baseUrl
|
|
2334
|
+
baseUrl,
|
|
1855
2335
|
}),
|
|
1856
|
-
|
|
2336
|
+
authGeneration,
|
|
2337
|
+
apiKey,
|
|
2338
|
+
endpoint: this.#normalizeDiscoveryEvidenceEndpoint(baseUrl ?? ""),
|
|
2339
|
+
});
|
|
1857
2340
|
}
|
|
1858
2341
|
}
|
|
1859
2342
|
|
|
1860
2343
|
for (let i = 0; i < enabledSpecialProviderDescriptors.length; i++) {
|
|
1861
2344
|
const descriptor = enabledSpecialProviderDescriptors[i];
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
2345
|
+
const { apiKey: apiKeyValue, authGeneration } = specialProviderCredentials[i];
|
|
2346
|
+
const key = descriptor.resolveKey(apiKeyValue);
|
|
2347
|
+
if (
|
|
2348
|
+
authGeneration !== undefined &&
|
|
2349
|
+
authGeneration === this.#getProviderEvidenceGeneration(descriptor.providerId, apiKeyValue) &&
|
|
2350
|
+
isAuthenticated(key)
|
|
2351
|
+
) {
|
|
2352
|
+
const managerOptions = descriptor.createOptions(key);
|
|
2353
|
+
options.push({
|
|
2354
|
+
options: managerOptions,
|
|
2355
|
+
authGeneration,
|
|
2356
|
+
apiKey: apiKeyValue,
|
|
2357
|
+
endpoint: this.#normalizeDiscoveryEvidenceEndpoint(
|
|
2358
|
+
this.#getProviderBaseUrlForDiscovery(descriptor.providerId) ?? "",
|
|
2359
|
+
),
|
|
2360
|
+
});
|
|
1865
2361
|
}
|
|
1866
|
-
options.push(descriptor.createOptions(key));
|
|
1867
2362
|
}
|
|
1868
2363
|
return options;
|
|
1869
2364
|
}
|
|
1870
2365
|
|
|
1871
2366
|
async #discoverWithModelManager(
|
|
1872
|
-
options:
|
|
2367
|
+
{ options, authGeneration, apiKey, endpoint }: ModelManagerDiscoveryOptions,
|
|
1873
2368
|
strategy: ModelRefreshStrategy,
|
|
1874
2369
|
): Promise<Model<Api>[]> {
|
|
2370
|
+
const generation = (this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) + 1;
|
|
2371
|
+
this.#descriptorDiscoveryGenerations.set(options.providerId, generation);
|
|
2372
|
+
const canUseCredentialDerivedXiaomiEndpoint =
|
|
2373
|
+
options.providerId === "xiaomi" &&
|
|
2374
|
+
apiKey?.startsWith("tp-") === true &&
|
|
2375
|
+
this.#runtimeProviderOverrides.get("xiaomi")?.baseUrl === undefined &&
|
|
2376
|
+
this.#providerOverrides.get("xiaomi")?.baseUrl === undefined &&
|
|
2377
|
+
resolveProviderBaseUrlFromEnv("xiaomi") === undefined;
|
|
2378
|
+
let credentialDerivedEndpoint: string | undefined;
|
|
2379
|
+
const isCurrentDiscovery = () =>
|
|
2380
|
+
(this.#descriptorDiscoveryGenerations.get(options.providerId) ?? 0) === generation &&
|
|
2381
|
+
this.#getProviderEvidenceGeneration(options.providerId, apiKey) === authGeneration &&
|
|
2382
|
+
(endpoint ===
|
|
2383
|
+
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(options.providerId) ?? "") ||
|
|
2384
|
+
(canUseCredentialDerivedXiaomiEndpoint && credentialDerivedEndpoint !== undefined));
|
|
1875
2385
|
try {
|
|
1876
|
-
const manager = createModelManager({
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2386
|
+
const manager = createModelManager({
|
|
2387
|
+
...options,
|
|
2388
|
+
cacheDbPath: this.#cacheDbPath,
|
|
2389
|
+
canPublishCache: isCurrentDiscovery,
|
|
2390
|
+
...(options.fetchDynamicModels
|
|
2391
|
+
? {
|
|
2392
|
+
fetchDynamicModels: async () => {
|
|
2393
|
+
const models = await options.fetchDynamicModels?.();
|
|
2394
|
+
if (models === null) return null;
|
|
2395
|
+
const sanitizedModels = this.#stripModelBaseUrlQueries(models ?? []);
|
|
2396
|
+
if (canUseCredentialDerivedXiaomiEndpoint) {
|
|
2397
|
+
credentialDerivedEndpoint = sanitizedModels[0]?.baseUrl;
|
|
2398
|
+
}
|
|
2399
|
+
return sanitizedModels;
|
|
2400
|
+
},
|
|
2401
|
+
}
|
|
2402
|
+
: {}),
|
|
2403
|
+
...(options.modelsDev
|
|
2404
|
+
? {
|
|
2405
|
+
modelsDev: {
|
|
2406
|
+
...options.modelsDev,
|
|
2407
|
+
map: (payload, providerId) =>
|
|
2408
|
+
this.#stripModelBaseUrlQueries(options.modelsDev?.map(payload, providerId) ?? []),
|
|
2409
|
+
},
|
|
2410
|
+
}
|
|
2411
|
+
: {}),
|
|
2412
|
+
});
|
|
2413
|
+
const evidence = this.#descriptorDiscoveryEvidence.get(options.providerId);
|
|
2414
|
+
const refreshStrategy =
|
|
2415
|
+
strategy === "online-if-uncached" &&
|
|
2416
|
+
(evidence?.authGeneration !== authGeneration || evidence.endpoint !== endpoint)
|
|
2417
|
+
? "online"
|
|
2418
|
+
: strategy;
|
|
2419
|
+
const result = await manager.refresh(refreshStrategy);
|
|
2420
|
+
const liveBaseUrl = this.#getProviderBaseUrlForDiscovery(options.providerId);
|
|
2421
|
+
const models = result.models.map(model => {
|
|
2422
|
+
const baseUrl = this.#restoreLiveDiscoveryBaseUrl(model.baseUrl, liveBaseUrl);
|
|
2423
|
+
return {
|
|
2424
|
+
...(model.provider === options.providerId ? model : { ...model, provider: options.providerId }),
|
|
2425
|
+
...(baseUrl !== model.baseUrl ? { baseUrl } : {}),
|
|
2426
|
+
};
|
|
2427
|
+
});
|
|
2428
|
+
if (
|
|
2429
|
+
isCurrentDiscovery() &&
|
|
2430
|
+
(result.fetched ||
|
|
2431
|
+
result.stale ||
|
|
2432
|
+
this.#descriptorDiscoveryEvidence.get(options.providerId)?.authGeneration !== authGeneration ||
|
|
2433
|
+
this.#descriptorDiscoveryEvidence.get(options.providerId)?.endpoint !== endpoint)
|
|
2434
|
+
) {
|
|
2435
|
+
this.#descriptorDiscoveryEvidence.set(options.providerId, {
|
|
2436
|
+
fresh: result.fetched,
|
|
2437
|
+
modelIds: new Set(models.map(model => model.id)),
|
|
2438
|
+
authGeneration,
|
|
2439
|
+
endpoint: this.#normalizeDiscoveryEvidenceEndpoint(models[0]?.baseUrl ?? endpoint),
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
if (!isCurrentDiscovery()) {
|
|
2443
|
+
return [];
|
|
2444
|
+
}
|
|
2445
|
+
this.#providerEvidenceApiKeys.set(options.providerId, apiKey);
|
|
2446
|
+
if (options.providerId === "opencodex" && !isAuthenticated(apiKey)) {
|
|
2447
|
+
this.#credentiallessAuthFallbackProviders.set(options.providerId, authGeneration);
|
|
2448
|
+
const evidence = this.#descriptorDiscoveryEvidence.get(options.providerId);
|
|
2449
|
+
if (evidence?.authGeneration === authGeneration) {
|
|
2450
|
+
this.#descriptorDiscoveryEvidence.set(options.providerId, {
|
|
2451
|
+
...evidence,
|
|
2452
|
+
authGeneration: this.#getProviderEvidenceGeneration(options.providerId),
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return models;
|
|
1881
2457
|
} catch (error) {
|
|
2458
|
+
if (isCurrentDiscovery()) {
|
|
2459
|
+
this.#providerEvidenceApiKeys.set(options.providerId, apiKey);
|
|
2460
|
+
this.#descriptorDiscoveryEvidence.set(options.providerId, {
|
|
2461
|
+
fresh: false,
|
|
2462
|
+
modelIds: new Set(),
|
|
2463
|
+
authGeneration,
|
|
2464
|
+
endpoint,
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
1882
2467
|
logger.warn("model discovery failed for provider", {
|
|
1883
2468
|
provider: options.providerId,
|
|
1884
2469
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -1938,10 +2523,21 @@ export class ModelRegistry {
|
|
|
1938
2523
|
}
|
|
1939
2524
|
}
|
|
1940
2525
|
|
|
1941
|
-
async #discoverOllamaModels(
|
|
2526
|
+
async #discoverOllamaModels(
|
|
2527
|
+
providerConfig: DiscoveryProviderConfig,
|
|
2528
|
+
discoveryApiKey?: string,
|
|
2529
|
+
): Promise<Model<Api>[]> {
|
|
1942
2530
|
const endpoint = this.#normalizeOllamaBaseUrl(providerConfig.baseUrl);
|
|
1943
2531
|
const tagsUrl = `${endpoint}/api/tags`;
|
|
1944
|
-
const headers = { ...(providerConfig.headers ?? {}) };
|
|
2532
|
+
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
2533
|
+
const apiKey =
|
|
2534
|
+
discoveryApiKey ??
|
|
2535
|
+
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
2536
|
+
? kNoAuth
|
|
2537
|
+
: await this.authStorage.getApiKey(providerConfig.provider));
|
|
2538
|
+
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
2539
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
2540
|
+
}
|
|
1945
2541
|
const response = await fetch(tagsUrl, {
|
|
1946
2542
|
headers,
|
|
1947
2543
|
signal: AbortSignal.timeout(250),
|
|
@@ -2006,12 +2602,19 @@ export class ModelRegistry {
|
|
|
2006
2602
|
}
|
|
2007
2603
|
}
|
|
2008
2604
|
|
|
2009
|
-
async #discoverLlamaCppModels(
|
|
2605
|
+
async #discoverLlamaCppModels(
|
|
2606
|
+
providerConfig: DiscoveryProviderConfig,
|
|
2607
|
+
discoveryApiKey?: string,
|
|
2608
|
+
): Promise<Model<Api>[]> {
|
|
2010
2609
|
const baseUrl = this.#normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
|
|
2011
2610
|
const modelsUrl = `${baseUrl}/models`;
|
|
2012
2611
|
|
|
2013
2612
|
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
2014
|
-
const apiKey =
|
|
2613
|
+
const apiKey =
|
|
2614
|
+
discoveryApiKey ??
|
|
2615
|
+
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
2616
|
+
? kNoAuth
|
|
2617
|
+
: await this.authStorage.getApiKey(providerConfig.provider));
|
|
2015
2618
|
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
2016
2619
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
2017
2620
|
}
|
|
@@ -2056,15 +2659,24 @@ export class ModelRegistry {
|
|
|
2056
2659
|
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
2057
2660
|
}
|
|
2058
2661
|
|
|
2059
|
-
async #discoverOpenAIModelsList(
|
|
2662
|
+
async #discoverOpenAIModelsList(
|
|
2663
|
+
providerConfig: DiscoveryProviderConfig,
|
|
2664
|
+
discoveryApiKey?: string,
|
|
2665
|
+
): Promise<Model<Api>[]> {
|
|
2060
2666
|
const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
|
|
2061
|
-
const modelsUrl =
|
|
2667
|
+
const modelsUrl = new URL(baseUrl);
|
|
2668
|
+
const requestBaseUrl = baseUrl;
|
|
2669
|
+
modelsUrl.pathname = `${modelsUrl.pathname.replace(/\/+$/g, "")}/models`;
|
|
2062
2670
|
|
|
2063
2671
|
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
2064
2672
|
// Resolve with the same baseUrl context completion requests use so an
|
|
2065
2673
|
// endpoint-scoped (or config-pinned) credential wins here exactly as it
|
|
2066
2674
|
// does for chat completions.
|
|
2067
|
-
const apiKey =
|
|
2675
|
+
const apiKey =
|
|
2676
|
+
discoveryApiKey ??
|
|
2677
|
+
(this.#isCredentiallessProvider(providerConfig.provider)
|
|
2678
|
+
? kNoAuth
|
|
2679
|
+
: await this.authStorage.getApiKey(providerConfig.provider, undefined, { baseUrl }));
|
|
2068
2680
|
if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
|
|
2069
2681
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
2070
2682
|
}
|
|
@@ -2078,10 +2690,10 @@ export class ModelRegistry {
|
|
|
2078
2690
|
// Redacted by construction: name the provider, endpoint, and the
|
|
2079
2691
|
// config surface to fix — never the resolved key.
|
|
2080
2692
|
throw new Error(
|
|
2081
|
-
`HTTP ${response.status} from ${modelsUrl}: provider "${providerConfig.provider}" credential was rejected for OpenAI models-list discovery; check providers.${providerConfig.provider}.apiKey/apiKeyEnv.`,
|
|
2693
|
+
`HTTP ${response.status} from ${redactDiscoveryUrl(modelsUrl)}: provider "${providerConfig.provider}" credential was rejected for OpenAI models-list discovery; check providers.${providerConfig.provider}.apiKey/apiKeyEnv.`,
|
|
2082
2694
|
);
|
|
2083
2695
|
}
|
|
2084
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
2696
|
+
throw new Error(`HTTP ${response.status} from ${redactDiscoveryUrl(modelsUrl)}`);
|
|
2085
2697
|
}
|
|
2086
2698
|
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
2087
2699
|
const models = payload.data ?? [];
|
|
@@ -2095,7 +2707,7 @@ export class ModelRegistry {
|
|
|
2095
2707
|
name: id,
|
|
2096
2708
|
api: providerConfig.api,
|
|
2097
2709
|
provider: providerConfig.provider,
|
|
2098
|
-
baseUrl,
|
|
2710
|
+
baseUrl: requestBaseUrl,
|
|
2099
2711
|
reasoning: false,
|
|
2100
2712
|
input: ["text"],
|
|
2101
2713
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
@@ -2124,7 +2736,6 @@ export class ModelRegistry {
|
|
|
2124
2736
|
return raw;
|
|
2125
2737
|
}
|
|
2126
2738
|
}
|
|
2127
|
-
|
|
2128
2739
|
#toLlamaCppNativeBaseUrl(baseUrl: string): string {
|
|
2129
2740
|
try {
|
|
2130
2741
|
const parsed = new URL(baseUrl);
|
|
@@ -2136,7 +2747,43 @@ export class ModelRegistry {
|
|
|
2136
2747
|
return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
|
|
2137
2748
|
}
|
|
2138
2749
|
}
|
|
2139
|
-
|
|
2750
|
+
#normalizeDiscoveryEvidenceEndpoint(endpoint: string): string {
|
|
2751
|
+
try {
|
|
2752
|
+
const parsed = new URL(endpoint);
|
|
2753
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2754
|
+
return `${parsed.protocol}//${parsed.host}${trimmedPath}${parsed.search}`;
|
|
2755
|
+
} catch {
|
|
2756
|
+
return endpoint.replace(/\/+$/g, "");
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
#isCredentiallessProvider(provider: string): boolean {
|
|
2760
|
+
let fallbackMatchesCurrentEvidence = false;
|
|
2761
|
+
const fallbackEvidenceGeneration = this.#credentiallessAuthFallbackProviders.get(provider);
|
|
2762
|
+
if (fallbackEvidenceGeneration !== undefined) {
|
|
2763
|
+
try {
|
|
2764
|
+
fallbackMatchesCurrentEvidence =
|
|
2765
|
+
fallbackEvidenceGeneration ===
|
|
2766
|
+
this.authStorage.getProviderEvidenceGeneration(provider, this.#providerEvidenceApiKeys.get(provider));
|
|
2767
|
+
} catch {
|
|
2768
|
+
// AuthStorage may be unavailable while a registry is being torn down.
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
return (
|
|
2772
|
+
this.#keylessProviders.has(provider) &&
|
|
2773
|
+
(!this.#optionalAuthProviders.has(provider) ||
|
|
2774
|
+
(!this.authStorage.hasAuth(provider) && !this.authStorage.has(provider)) ||
|
|
2775
|
+
fallbackMatchesCurrentEvidence)
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
#getProviderEvidenceGeneration(provider: string, resolvedApiKey?: string): string {
|
|
2779
|
+
if (this.#isCredentiallessProvider(provider)) {
|
|
2780
|
+
return `credentialless:${provider}`;
|
|
2781
|
+
}
|
|
2782
|
+
return this.authStorage.getProviderEvidenceGeneration(
|
|
2783
|
+
provider,
|
|
2784
|
+
resolvedApiKey ?? this.#providerEvidenceApiKeys.get(provider),
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2140
2787
|
#normalizeOpenAIModelsListBaseUrl(baseUrl?: string): string {
|
|
2141
2788
|
const defaultBaseUrl = "http://127.0.0.1:1234/v1";
|
|
2142
2789
|
const raw = baseUrl || defaultBaseUrl;
|
|
@@ -2144,7 +2791,7 @@ export class ModelRegistry {
|
|
|
2144
2791
|
const parsed = new URL(raw);
|
|
2145
2792
|
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2146
2793
|
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
|
|
2147
|
-
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2794
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}${parsed.search}`;
|
|
2148
2795
|
} catch {
|
|
2149
2796
|
return raw;
|
|
2150
2797
|
}
|
|
@@ -2189,6 +2836,7 @@ export class ModelRegistry {
|
|
|
2189
2836
|
|
|
2190
2837
|
#getProviderBaseUrlForDiscovery(provider: string): string | undefined {
|
|
2191
2838
|
return (
|
|
2839
|
+
this.#runtimeProviderOverrides.get(provider)?.baseUrl ??
|
|
2192
2840
|
this.#providerOverrides.get(provider)?.baseUrl ??
|
|
2193
2841
|
resolveProviderBaseUrlFromEnv(provider) ??
|
|
2194
2842
|
this.getProviderBaseUrl(provider)
|
|
@@ -2505,6 +3153,103 @@ export class ModelRegistry {
|
|
|
2505
3153
|
this.#availableModelsEnvFingerprint = envFingerprint;
|
|
2506
3154
|
return this.#availableModelsCache;
|
|
2507
3155
|
}
|
|
3156
|
+
#hasFreshOrStaticModelEvidence(model: Model<Api>): boolean {
|
|
3157
|
+
const evidence = this.#providerActivity.get(model.provider);
|
|
3158
|
+
if (
|
|
3159
|
+
!evidence ||
|
|
3160
|
+
(!evidence.staticConfigured &&
|
|
3161
|
+
!evidence.discoveryConfigured &&
|
|
3162
|
+
!evidence.implicitDiscovery &&
|
|
3163
|
+
!evidence.descriptorBacked)
|
|
3164
|
+
) {
|
|
3165
|
+
return false;
|
|
3166
|
+
}
|
|
3167
|
+
if (evidence.staticConfigured && (!evidence.discoveryConfigured || evidence.staticModelIds.has(model.id))) {
|
|
3168
|
+
return true;
|
|
3169
|
+
}
|
|
3170
|
+
if (
|
|
3171
|
+
evidence.descriptorFresh &&
|
|
3172
|
+
evidence.authGeneration === this.#getProviderEvidenceGeneration(model.provider) &&
|
|
3173
|
+
evidence.endpoint ===
|
|
3174
|
+
this.#normalizeDiscoveryEvidenceEndpoint(
|
|
3175
|
+
this.#getProviderBaseUrlForDiscovery(model.provider) ?? model.baseUrl ?? "",
|
|
3176
|
+
) &&
|
|
3177
|
+
evidence.descriptorModelIds.has(model.id)
|
|
3178
|
+
)
|
|
3179
|
+
return true;
|
|
3180
|
+
const discoveryState = this.#discoveryManager.getState(model.provider);
|
|
3181
|
+
const configuredEvidence = this.#configuredDiscoveryEvidence.get(model.provider);
|
|
3182
|
+
return (
|
|
3183
|
+
(discoveryState?.status === "ok" || discoveryState?.status === "cached") &&
|
|
3184
|
+
configuredEvidence?.authGeneration === this.#getProviderEvidenceGeneration(model.provider) &&
|
|
3185
|
+
configuredEvidence.endpoint ===
|
|
3186
|
+
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(model.provider) ?? "") &&
|
|
3187
|
+
configuredEvidence.modelIds.has(model.id)
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
#activeConnectionKind(model: Model<Api>): ActiveProviderDescriptor["connectionKind"] | undefined {
|
|
3192
|
+
const evidence = this.#providerActivity.get(model.provider);
|
|
3193
|
+
if (!this.#isCredentiallessProvider(model.provider) && this.authStorage.hasUsableAuth(model.provider)) {
|
|
3194
|
+
if (!evidence) return undefined;
|
|
3195
|
+
const discoveryOnly =
|
|
3196
|
+
!evidence.staticConfigured &&
|
|
3197
|
+
(evidence.discoveryConfigured || evidence.implicitDiscovery || evidence.descriptorBacked);
|
|
3198
|
+
return !discoveryOnly || this.#hasFreshOrStaticModelEvidence(model) ? "credential" : undefined;
|
|
3199
|
+
}
|
|
3200
|
+
if (this.#isCredentiallessProvider(model.provider)) {
|
|
3201
|
+
const discoveryOnly =
|
|
3202
|
+
evidence !== undefined &&
|
|
3203
|
+
!evidence.staticConfigured &&
|
|
3204
|
+
(evidence.discoveryConfigured || evidence.implicitDiscovery || evidence.descriptorBacked);
|
|
3205
|
+
if (discoveryOnly) {
|
|
3206
|
+
if (evidence.descriptorBacked)
|
|
3207
|
+
return this.#hasFreshOrStaticModelEvidence(model) ? "credentialless" : undefined;
|
|
3208
|
+
const configuredBaseUrl = this.#getProviderBaseUrlForDiscovery(model.provider);
|
|
3209
|
+
const discoveryType = this.#discoveryManager.providers.find(
|
|
3210
|
+
provider => provider.provider === model.provider,
|
|
3211
|
+
)?.discovery.type;
|
|
3212
|
+
const endpointFor = (baseUrl: string | undefined): string =>
|
|
3213
|
+
this.#normalizeDiscoveryEvidenceEndpoint(
|
|
3214
|
+
discoveryType === "ollama"
|
|
3215
|
+
? `${this.#normalizeOllamaBaseUrl(baseUrl)}/v1`
|
|
3216
|
+
: discoveryType === "openai-models-list" || discoveryType === "lm-studio"
|
|
3217
|
+
? this.#normalizeOpenAIModelsListBaseUrl(baseUrl)
|
|
3218
|
+
: (baseUrl ?? ""),
|
|
3219
|
+
);
|
|
3220
|
+
if (
|
|
3221
|
+
endpointFor(model.baseUrl) !== endpointFor(configuredBaseUrl) ||
|
|
3222
|
+
this.#discoveryManager.getState(model.provider)?.status === "empty"
|
|
3223
|
+
)
|
|
3224
|
+
return undefined;
|
|
3225
|
+
const configuredEvidence = this.#configuredDiscoveryEvidence.get(model.provider);
|
|
3226
|
+
if (
|
|
3227
|
+
this.#discoveryManager.getState(model.provider)?.error !== undefined ||
|
|
3228
|
+
(this.#optionalAuthProviders.has(model.provider) &&
|
|
3229
|
+
(configuredEvidence === undefined ||
|
|
3230
|
+
configuredEvidence.authGeneration !== this.#getProviderEvidenceGeneration(model.provider)))
|
|
3231
|
+
)
|
|
3232
|
+
return undefined;
|
|
3233
|
+
}
|
|
3234
|
+
return "credentialless";
|
|
3235
|
+
}
|
|
3236
|
+
return undefined;
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
getActiveProviders(): ActiveProviderDescriptor[] {
|
|
3240
|
+
try {
|
|
3241
|
+
const descriptors: ActiveProviderDescriptor[] = [];
|
|
3242
|
+
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
3243
|
+
const available = this.#models.filter(model => this.#isModelAvailable(model, disabledProviders));
|
|
3244
|
+
for (const model of available) {
|
|
3245
|
+
const connectionKind = this.#activeConnectionKind(model);
|
|
3246
|
+
if (connectionKind) descriptors.push({ provider: model.provider, connectionKind });
|
|
3247
|
+
}
|
|
3248
|
+
return projectActiveProviderDescriptors(descriptors);
|
|
3249
|
+
} catch {
|
|
3250
|
+
throw new ActiveProviderResolutionError();
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
2508
3253
|
|
|
2509
3254
|
/**
|
|
2510
3255
|
* Check whether auth is configured for a model's provider.
|
|
@@ -2571,10 +3316,14 @@ export class ModelRegistry {
|
|
|
2571
3316
|
}
|
|
2572
3317
|
|
|
2573
3318
|
async #getApiKeyOrNoAuth(provider: string, lookup: () => Promise<string | undefined>): Promise<string | undefined> {
|
|
2574
|
-
if (this.#
|
|
2575
|
-
|
|
3319
|
+
if (!this.#isCredentiallessProvider(provider)) return lookup();
|
|
3320
|
+
if (!this.#optionalAuthProviders.has(provider)) return kNoAuth;
|
|
3321
|
+
const apiKey = await lookup();
|
|
3322
|
+
if (apiKey !== undefined) {
|
|
3323
|
+
this.#credentiallessAuthFallbackProviders.delete(provider);
|
|
3324
|
+
return apiKey;
|
|
2576
3325
|
}
|
|
2577
|
-
return
|
|
3326
|
+
return kNoAuth;
|
|
2578
3327
|
}
|
|
2579
3328
|
|
|
2580
3329
|
/**
|
|
@@ -2613,8 +3362,28 @@ export class ModelRegistry {
|
|
|
2613
3362
|
);
|
|
2614
3363
|
}
|
|
2615
3364
|
|
|
2616
|
-
async #peekApiKeyForProvider(
|
|
2617
|
-
|
|
3365
|
+
async #peekApiKeyForProvider(
|
|
3366
|
+
provider: string,
|
|
3367
|
+
options: {
|
|
3368
|
+
ignoreCredentiallessFallback?: boolean;
|
|
3369
|
+
refreshOAuth?: boolean;
|
|
3370
|
+
baseUrl?: string;
|
|
3371
|
+
} = {},
|
|
3372
|
+
): Promise<string | undefined> {
|
|
3373
|
+
if (!options.ignoreCredentiallessFallback && this.#isCredentiallessProvider(provider)) {
|
|
3374
|
+
return kNoAuth;
|
|
3375
|
+
}
|
|
3376
|
+
try {
|
|
3377
|
+
this.authStorage.getProviderEvidenceGeneration(provider);
|
|
3378
|
+
} catch {
|
|
3379
|
+
return undefined;
|
|
3380
|
+
}
|
|
3381
|
+
if (options.refreshOAuth && this.authStorage.hasOAuth(provider)) {
|
|
3382
|
+
return this.authStorage.getApiKey(provider, undefined, { baseUrl: options.baseUrl });
|
|
3383
|
+
}
|
|
3384
|
+
return options.ignoreCredentiallessFallback
|
|
3385
|
+
? this.authStorage.peekApiKey(provider)
|
|
3386
|
+
: this.#getApiKeyOrNoAuth(provider, () => this.authStorage.peekApiKey(provider));
|
|
2618
3387
|
}
|
|
2619
3388
|
|
|
2620
3389
|
/**
|
|
@@ -2628,11 +3397,22 @@ export class ModelRegistry {
|
|
|
2628
3397
|
return this.authStorage.getSessionCredentialType(provider, sessionId);
|
|
2629
3398
|
}
|
|
2630
3399
|
|
|
3400
|
+
#clearDescriptorDiscoveryEvidence(providerName: string): void {
|
|
3401
|
+
this.#descriptorDiscoveryGenerations.set(
|
|
3402
|
+
providerName,
|
|
3403
|
+
(this.#descriptorDiscoveryGenerations.get(providerName) ?? 0) + 1,
|
|
3404
|
+
);
|
|
3405
|
+
this.#descriptorDiscoveryEvidence.delete(providerName);
|
|
3406
|
+
this.#configuredDiscoveryEvidence.delete(providerName);
|
|
3407
|
+
this.#discoveryManager.invalidate(providerName);
|
|
3408
|
+
}
|
|
3409
|
+
|
|
2631
3410
|
#clearRuntimeProviderState(providerName: string): void {
|
|
2632
3411
|
this.#runtimeProviderApiKeys.delete(providerName);
|
|
2633
3412
|
this.#runtimeProviderOverrides.delete(providerName);
|
|
2634
3413
|
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
|
|
2635
3414
|
this.authStorage.removeConfigApiKey(providerName);
|
|
3415
|
+
this.#clearDescriptorDiscoveryEvidence(providerName);
|
|
2636
3416
|
}
|
|
2637
3417
|
|
|
2638
3418
|
/**
|
|
@@ -2654,6 +3434,7 @@ export class ModelRegistry {
|
|
|
2654
3434
|
this.#clearRuntimeProviderState(providerName);
|
|
2655
3435
|
}
|
|
2656
3436
|
this.#lastStaticLoadMtime = null;
|
|
3437
|
+
this.#staticModelsLoaded = false;
|
|
2657
3438
|
this.#reloadStaticModels();
|
|
2658
3439
|
this.#rebuildCanonicalIndex();
|
|
2659
3440
|
}
|
|
@@ -2698,6 +3479,8 @@ export class ModelRegistry {
|
|
|
2698
3479
|
},
|
|
2699
3480
|
"runtime-register",
|
|
2700
3481
|
);
|
|
3482
|
+
this.#clearDescriptorDiscoveryEvidence(providerName);
|
|
3483
|
+
this.#rebuildProviderActivity();
|
|
2701
3484
|
|
|
2702
3485
|
if (config.streamSimple && config.api) {
|
|
2703
3486
|
const streamSimple = config.streamSimple;
|
|
@@ -2734,6 +3517,7 @@ export class ModelRegistry {
|
|
|
2734
3517
|
}
|
|
2735
3518
|
if (sourceHandoff) {
|
|
2736
3519
|
this.#lastStaticLoadMtime = null;
|
|
3520
|
+
this.#staticModelsLoaded = false;
|
|
2737
3521
|
this.#reloadStaticModels();
|
|
2738
3522
|
}
|
|
2739
3523
|
|
|
@@ -2791,12 +3575,14 @@ export class ModelRegistry {
|
|
|
2791
3575
|
config.oauth.modifyModels(withRuntimeTransportOverride, credential),
|
|
2792
3576
|
);
|
|
2793
3577
|
this.#rebuildCanonicalIndex();
|
|
3578
|
+
this.#rebuildProviderActivity();
|
|
2794
3579
|
return;
|
|
2795
3580
|
}
|
|
2796
3581
|
}
|
|
2797
3582
|
|
|
2798
3583
|
this.#models = applyFinalCodexGpt56ContextCap(withRuntimeTransportOverride);
|
|
2799
3584
|
this.#rebuildCanonicalIndex();
|
|
3585
|
+
this.#rebuildProviderActivity();
|
|
2800
3586
|
return;
|
|
2801
3587
|
}
|
|
2802
3588
|
|
|
@@ -2826,6 +3612,7 @@ export class ModelRegistry {
|
|
|
2826
3612
|
return this.#applyProviderTransportOverride(m, transportOverride);
|
|
2827
3613
|
});
|
|
2828
3614
|
this.#rebuildCanonicalIndex();
|
|
3615
|
+
this.#rebuildProviderActivity();
|
|
2829
3616
|
}
|
|
2830
3617
|
}
|
|
2831
3618
|
|