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