{"version":3,"file":"capacity-probe.d.ts","sourceRoot":"","sources":["../../../src/core/shared-inference/capacity-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,4BAA4B,GACrC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,4BAA4B,CAAA;CAAE,GACjE;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,WAAW,sBAAsB;IACtC,KAAK,CAAC,QAAQ,EAAE,uBAAuB,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAChF;AAED,MAAM,WAAW,4BAA4B;IAC5C,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,qBAAa,qBAAsB,YAAW,sBAAsB;IACnE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAA+C;IACxE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IAEpC,YAAY,OAAO,GAAE,4BAAiC,EAKrD;IAEK,KAAK,CAAC,QAAQ,EAAE,uBAAuB,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAkCpF;YAEa,IAAI;CAWlB","sourcesContent":["/**\n * Inference capacity probe (3.0.0 foundation).\n *\n * Optional backend telemetry adapter. Physical capacity belongs to the backend,\n * not to any agent/assignment/worker. For llama.cpp-compatible servers we can\n * observe `total_slots` from `/props` and per-slot `is_processing` from `/slots`\n * reliably. When a backend does not expose these, the caller reports\n * NOT_OBSERVED instead of guessing.\n *\n * Credentials are resolved per-probe and never persisted by the scheduler.\n */\n\nimport type { SharedInferenceResource } from \"./types.js\";\n\nexport interface InferenceCapacityObservation {\n\tcapacity?: number;\n\tbusySlots?: number;\n\tcontextWindow?: number;\n\tobservedAtMs: number;\n}\n\nexport type InferenceCapacityProbeResult =\n\t| { status: \"observed\"; observation: InferenceCapacityObservation }\n\t| { status: \"not_observed\"; reason: string };\n\nexport interface InferenceCapacityProbe {\n\tprobe(resource: SharedInferenceResource): Promise<InferenceCapacityProbeResult>;\n}\n\nexport interface LlamaCppCapacityProbeOptions {\n\t/** Resolve the bearer key at probe time (never persisted). */\n\tapiKey?: string | (() => Promise<string | undefined>);\n\tfetchImpl?: typeof fetch;\n\ttimeoutMs?: number;\n\tnow?: () => number;\n}\n\n/**\n * llama.cpp server `/props` + `/slots` observation.\n *\n * `/props` exposes `total_slots` (the authoritative physical slot count) and\n * `default_generation_settings.n_ctx` (context window). `/slots` exposes each\n * slot's `is_processing` flag, from which busy slots are derived.\n */\nexport class LlamaCppCapacityProbe implements InferenceCapacityProbe {\n\tprivate readonly _apiKey?: string | (() => Promise<string | undefined>);\n\tprivate readonly _fetchImpl: typeof fetch;\n\tprivate readonly _timeoutMs: number;\n\tprivate readonly _now: () => number;\n\n\tconstructor(options: LlamaCppCapacityProbeOptions = {}) {\n\t\tthis._apiKey = options.apiKey;\n\t\tthis._fetchImpl = options.fetchImpl ?? fetch;\n\t\tthis._timeoutMs = options.timeoutMs ?? 3000;\n\t\tthis._now = options.now ?? (() => Date.now());\n\t}\n\n\tasync probe(resource: SharedInferenceResource): Promise<InferenceCapacityProbeResult> {\n\t\tif (!resource.baseUrl) return { status: \"not_observed\", reason: \"no_base_url\" };\n\n\t\tconst apiKey = typeof this._apiKey === \"function\" ? await this._apiKey() : this._apiKey;\n\t\tconst headers: Record<string, string> = {};\n\t\tif (apiKey) headers.Authorization = `Bearer ${apiKey}`;\n\n\t\ttry {\n\t\t\tconst [props, slots] = await Promise.all([\n\t\t\t\tthis._get(`${resource.baseUrl}/props`, headers),\n\t\t\t\tthis._get(`${resource.baseUrl}/slots`, headers),\n\t\t\t]);\n\n\t\t\tconst capacity =\n\t\t\t\ttypeof props?.total_slots === \"number\" && props.total_slots >= 1 ? props.total_slots : undefined;\n\t\t\tconst contextWindow =\n\t\t\t\ttypeof props?.default_generation_settings?.n_ctx === \"number\"\n\t\t\t\t\t? props.default_generation_settings.n_ctx\n\t\t\t\t\t: undefined;\n\t\t\tconst busySlots = Array.isArray(slots) ? slots.filter((s) => s?.is_processing === true).length : undefined;\n\n\t\t\tif (capacity === undefined && busySlots === undefined && contextWindow === undefined) {\n\t\t\t\treturn { status: \"not_observed\", reason: \"backend_endpoints_without_capacity_signals\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: \"observed\",\n\t\t\t\tobservation: { capacity, busySlots, contextWindow, observedAtMs: this._now() },\n\t\t\t};\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tstatus: \"not_observed\",\n\t\t\t\treason: error instanceof Error ? error.message : String(error),\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate async _get(url: string, headers: Record<string, string>): Promise<any> {\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), this._timeoutMs);\n\t\ttry {\n\t\t\tconst response = await this._fetchImpl(url, { headers, signal: controller.signal });\n\t\t\tif (!response.ok) return undefined;\n\t\t\treturn (await response.json()) as unknown;\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t}\n}\n"]}