import { isIP } from 'node:net' import { readFile } from 'node:fs/promises' import { durableReplace } from '../../../platform/lib/atomic.mjs' import { canonicalJson } from '../../../platform/lib/canonical-json.mjs' import { validProviderId } from '../outbound-proxy/lib/contracts.mjs' const CACHE_SCHEMA = 1 const DEFAULT_ENDPOINT = 'http://127.0.0.1:3079' const RPC_TIMEOUT_MS = 5_000 function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) } function rpcValue(value, method) { if (!object(value) || value.type !== 'server-response' || !object(value.result)) { throw new Error(`DSH ${method} response is invalid`) } if (value.result.ok !== true || !Object.hasOwn(value.result, 'value')) { throw new Error(`DSH ${method} request failed`) } return value.result.value } async function rpc(fetchImpl, endpoint, method, signal, headers) { const rpcId = `dsh-proxy-${method}` const response = await fetchImpl(`${endpoint}/api/${method}`, { method: 'POST', headers: { ...headers, 'content-type': 'application/json' }, body: JSON.stringify({ type: 'client-request', rpcId, method, payload: { args: {} } }), signal, }) if (!response.ok) throw new Error(`DSH ${method} returned HTTP ${String(response.status)}`) return rpcValue(await response.json(), method) } function nestedValue(value, path) { let current = value for (const part of path) { if (!object(current) || !(part in current)) return undefined current = current[part] } return current } function baseUrlFor(provider, namespaces) { const namespace = namespaces.get(provider.settingsNs) if (namespace === undefined) return undefined const path = Array.isArray(provider.settingsPath) ? provider.settingsPath : [] const configured = nestedValue(namespace.value, path) const defaults = nestedValue(namespace.base, path) for (const candidate of [configured, defaults]) { if (object(candidate) && typeof candidate.baseURL === 'string' && candidate.baseURL.trim() !== '') { return candidate.baseURL.trim() } } return undefined } function configuredProvider(provider, namespaces) { const namespace = namespaces.get(provider.settingsNs) if (namespace === undefined) return false const path = Array.isArray(provider.settingsPath) ? provider.settingsPath : [] return path.length === 0 || nestedValue(namespace.value, path) !== undefined } function loopbackUrl(value) { if (value === undefined) return false let parsed try { parsed = new URL(value) } catch { return false } const host = parsed.hostname.startsWith('[') && parsed.hostname.endsWith(']') ? parsed.hostname.slice(1, -1) : parsed.hostname.toLowerCase() if (host === 'localhost' || host.endsWith('.localhost')) return true if (isIP(host) === 4) return host.startsWith('127.') if (isIP(host) === 6) return host === '::1' || host === '0:0:0:0:0:0:0:1' return false } function sanitizeProvider(provider, namespaces) { if (!object(provider) || !validProviderId(provider.provider)) return undefined if (!configuredProvider(provider, namespaces)) return undefined const local = loopbackUrl(baseUrlFor(provider, namespaces)) const routingCapability = local ? 'forced-direct' : provider.routingCapability === 'shared-dsh' || provider.supportsIndependentRouting === false ? 'shared-dsh' : 'provider' return Object.freeze({ id: provider.provider, displayName: typeof provider.displayName === 'string' && provider.displayName.trim() !== '' ? provider.displayName.trim() : provider.provider, type: provider.provider, active: provider.active === true, declared: provider.declared !== false, configured: true, routingCapability, reason: routingCapability === 'forced-direct' ? 'local-provider' : routingCapability === 'shared-dsh' ? 'client-uses-shared-dsh-route' : null, }) } function decorate(provider, snapshot) { if (provider.routingCapability !== 'forced-direct') { const requestedPolicy = snapshot.configuration.modelApi.providers[provider.id] ?? snapshot.configuration.modelApi.default return Object.freeze({ ...provider, requestedPolicy, effectivePolicy: requestedPolicy.proxyEnabled ? provider.routingCapability === 'shared-dsh' ? 'shared-dsh' : 'proxy' : 'direct', }) } return Object.freeze({ ...provider, requestedPolicy: null, effectivePolicy: 'direct', }) } function validateCache(value) { if (!object(value) || value.schema !== CACHE_SCHEMA || !Array.isArray(value.providers) || typeof value.updatedAt !== 'string') { throw new Error('cached Provider inventory is invalid') } const providers = value.providers.map(provider => { if (!object(provider) || !validProviderId(provider.id) || typeof provider.displayName !== 'string' || typeof provider.type !== 'string' || typeof provider.active !== 'boolean' || typeof provider.declared !== 'boolean' || provider.configured !== true || !['provider', 'shared-dsh', 'forced-direct'].includes(provider.routingCapability) || !(provider.reason === null || typeof provider.reason === 'string')) { throw new Error('cached Provider inventory entry is invalid') } return Object.freeze({ ...provider }) }) return Object.freeze({ schema: CACHE_SCHEMA, updatedAt: value.updatedAt, providers: Object.freeze(providers) }) } export class ProviderInventory { constructor({ cachePath, endpoint = DEFAULT_ENDPOINT, fetchImpl = fetch, authentication = Object.freeze({ cookie: async () => null }), timeoutMs = RPC_TIMEOUT_MS, now = () => new Date(), }) { if (typeof cachePath !== 'string' || cachePath === '') throw new TypeError('Provider inventory cache path is required') if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new TypeError('Provider inventory timeout is invalid') if (!object(authentication) || typeof authentication.cookie !== 'function') { throw new TypeError('Provider inventory authentication is invalid') } this.cachePath = cachePath this.endpoint = endpoint.replace(/\/$/, '') this.fetchImpl = fetchImpl this.authentication = authentication this.timeoutMs = timeoutMs this.now = now } async refresh() { const signal = AbortSignal.timeout(this.timeoutMs) const cookie = await this.authentication.cookie() if (!(cookie === null || typeof cookie === 'string')) throw new Error('DSH authentication cookie is invalid') const headers = cookie === null ? {} : { cookie } const [registeredValue, directoryValue, settingsValue] = await Promise.all([ rpc(this.fetchImpl, this.endpoint, 'llm/listProviders', signal, headers), rpc(this.fetchImpl, this.endpoint, 'llm/listConfigurableProviders', signal, headers), rpc(this.fetchImpl, this.endpoint, 'settings/describe', signal, headers), ]) if (!Array.isArray(registeredValue) || !Array.isArray(directoryValue) || !Array.isArray(settingsValue.namespaces)) { throw new Error('DSH Provider inventory response is invalid') } const namespaces = new Map(settingsValue.namespaces .filter(namespace => object(namespace) && typeof namespace.ns === 'string') .map(namespace => [namespace.ns, namespace])) const active = new Set(registeredValue .filter(provider => object(provider) && validProviderId(provider.id)) .map(provider => provider.id)) const providers = directoryValue .map(provider => sanitizeProvider({ ...provider, active: active.has(provider?.provider) }, namespaces)) .filter(provider => provider !== undefined) .sort((left, right) => left.displayName.localeCompare(right.displayName) || left.id.localeCompare(right.id)) const cache = Object.freeze({ schema: CACHE_SCHEMA, updatedAt: this.now().toISOString(), providers: Object.freeze(providers), }) await durableReplace(this.cachePath, canonicalJson(cache)) return cache } async cached() { return validateCache(JSON.parse(await readFile(this.cachePath, 'utf8'))) } async list(snapshot, { refresh = true } = {}) { let cache let source = 'live' let error = null if (refresh) { try { cache = await this.refresh() } catch (failure) { source = 'cache' error = failure instanceof Error ? failure.message : String(failure) } } if (cache === undefined) { try { cache = await this.cached() } catch (failure) { return Object.freeze({ schema: CACHE_SCHEMA, source: 'unavailable', updatedAt: null, error: error ?? (failure instanceof Error ? failure.message : String(failure)), providers: Object.freeze([]), }) } } return Object.freeze({ schema: CACHE_SCHEMA, source, updatedAt: cache.updatedAt, error, providers: Object.freeze(cache.providers.map(provider => decorate(provider, snapshot))), }) } } export const providerInventoryInternals = Object.freeze({ baseUrlFor, configuredProvider, loopbackUrl, validateCache })