audit-tools 0.34.41 → 0.35.0
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/dist/audit/cli/args.d.ts +6 -0
- package/dist/audit/cli/args.d.ts.map +1 -1
- package/dist/audit/cli/args.js +21 -4
- package/dist/audit/cli/args.js.map +1 -1
- package/dist/audit/cli/auditStep.d.ts +6 -15
- package/dist/audit/cli/auditStep.d.ts.map +1 -1
- package/dist/audit/cli/auditStep.js +1 -1
- package/dist/audit/cli/auditStep.js.map +1 -1
- package/dist/audit/cli/dispatch/hostFanoutGate.d.ts +21 -61
- package/dist/audit/cli/dispatch/hostFanoutGate.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/hostFanoutGate.js +18 -167
- package/dist/audit/cli/dispatch/hostFanoutGate.js.map +1 -1
- package/dist/audit/cli/dispatch/packetFilter.d.ts +2 -2
- package/dist/audit/cli/dispatch/packetFilter.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/packetFilter.js +3 -1
- package/dist/audit/cli/dispatch/packetFilter.js.map +1 -1
- package/dist/audit/cli/dispatch/quotaPool.d.ts +2 -14
- package/dist/audit/cli/dispatch/quotaPool.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/quotaPool.js +16 -6
- package/dist/audit/cli/dispatch/quotaPool.js.map +1 -1
- package/dist/audit/cli/dispatch/tierRouting.d.ts +2 -2
- package/dist/audit/cli/dispatch/tokenUsageObservation.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/tokenUsageObservation.js +10 -2
- package/dist/audit/cli/dispatch/tokenUsageObservation.js.map +1 -1
- package/dist/audit/cli/dispatch.d.ts +14 -3
- package/dist/audit/cli/dispatch.d.ts.map +1 -1
- package/dist/audit/cli/dispatch.js +87 -65
- package/dist/audit/cli/dispatch.js.map +1 -1
- package/dist/audit/cli/forceSynthesisCommand.d.ts.map +1 -1
- package/dist/audit/cli/forceSynthesisCommand.js +9 -1
- package/dist/audit/cli/forceSynthesisCommand.js.map +1 -1
- package/dist/audit/cli/hybridDispatch.d.ts +4 -9
- package/dist/audit/cli/hybridDispatch.d.ts.map +1 -1
- package/dist/audit/cli/hybridDispatch.js +1 -1
- package/dist/audit/cli/hybridDispatch.js.map +1 -1
- package/dist/audit/cli/lineIndex.d.ts +2 -0
- package/dist/audit/cli/lineIndex.d.ts.map +1 -1
- package/dist/audit/cli/lineIndex.js +4 -0
- package/dist/audit/cli/lineIndex.js.map +1 -1
- package/dist/audit/cli/nextStepCommand.d.ts +0 -26
- package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
- package/dist/audit/cli/nextStepCommand.js +14 -358
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts +5 -16
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +27 -157
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/quotaCommand.d.ts.map +1 -1
- package/dist/audit/cli/quotaCommand.js +1 -5
- package/dist/audit/cli/quotaCommand.js.map +1 -1
- package/dist/audit/cli/reviewRun.d.ts.map +1 -1
- package/dist/audit/cli/reviewRun.js +58 -23
- package/dist/audit/cli/reviewRun.js.map +1 -1
- package/dist/audit/cli/rollingAuditDispatch.d.ts +1 -1
- package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
- package/dist/audit/cli/semanticReviewStep.js +35 -54
- package/dist/audit/cli/semanticReviewStep.js.map +1 -1
- package/dist/audit/cli/steps.d.ts +4 -4
- package/dist/audit/cli/steps.d.ts.map +1 -1
- package/dist/audit/cli/steps.js +0 -1
- package/dist/audit/cli/steps.js.map +1 -1
- package/dist/audit/cli/workPartitionRuntime.d.ts +11 -0
- package/dist/audit/cli/workPartitionRuntime.d.ts.map +1 -0
- package/dist/audit/cli/workPartitionRuntime.js +39 -0
- package/dist/audit/cli/workPartitionRuntime.js.map +1 -0
- package/dist/audit/contracts/wrapperResponse.d.ts +6 -6
- package/dist/audit/io/artifacts.d.ts +1 -3
- package/dist/audit/io/artifacts.d.ts.map +1 -1
- package/dist/audit/io/artifacts.js +0 -3
- package/dist/audit/io/artifacts.js.map +1 -1
- package/dist/audit/orchestrator/advance.d.ts.map +1 -1
- package/dist/audit/orchestrator/advance.js +0 -35
- package/dist/audit/orchestrator/advance.js.map +1 -1
- package/dist/audit/orchestrator/advanceTypes.d.ts +4 -63
- package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.js +9 -12
- package/dist/audit/orchestrator/executorRunners.js.map +1 -1
- package/dist/audit/orchestrator/executors.d.ts.map +1 -1
- package/dist/audit/orchestrator/executors.js +0 -5
- package/dist/audit/orchestrator/executors.js.map +1 -1
- package/dist/audit/orchestrator/hostInputPause.d.ts +1 -1
- package/dist/audit/orchestrator/hostInputPause.js +1 -1
- package/dist/audit/orchestrator/ingestionExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/ingestionExecutors.js +4 -1
- package/dist/audit/orchestrator/ingestionExecutors.js.map +1 -1
- package/dist/audit/orchestrator/intakeExecutors.d.ts +1 -65
- package/dist/audit/orchestrator/intakeExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/intakeExecutors.js +3 -425
- package/dist/audit/orchestrator/intakeExecutors.js.map +1 -1
- package/dist/audit/orchestrator/nextStep.d.ts +0 -13
- package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
- package/dist/audit/orchestrator/nextStep.js +0 -7
- package/dist/audit/orchestrator/nextStep.js.map +1 -1
- package/dist/audit/orchestrator/planningExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/planningExecutors.js +4 -1
- package/dist/audit/orchestrator/planningExecutors.js.map +1 -1
- package/dist/audit/orchestrator/rollingDispatch.d.ts +1 -1
- package/dist/audit/orchestrator/rollingDispatch.d.ts.map +1 -1
- package/dist/audit/orchestrator/rollingDispatch.js +5 -4
- package/dist/audit/orchestrator/rollingDispatch.js.map +1 -1
- package/dist/audit/orchestrator/state.d.ts +0 -39
- package/dist/audit/orchestrator/state.d.ts.map +1 -1
- package/dist/audit/orchestrator/state.js +3 -29
- package/dist/audit/orchestrator/state.js.map +1 -1
- package/dist/audit/orchestrator/synthesisExecutors.d.ts +8 -2
- package/dist/audit/orchestrator/synthesisExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/synthesisExecutors.js +7 -5
- package/dist/audit/orchestrator/synthesisExecutors.js.map +1 -1
- package/dist/audit/reporting/findingIdentity.d.ts +2 -2
- package/dist/audit/reporting/findingIdentity.js +2 -2
- package/dist/audit/reporting/synthesis.d.ts +7 -1
- package/dist/audit/reporting/synthesis.d.ts.map +1 -1
- package/dist/audit/reporting/synthesis.js +27 -7
- package/dist/audit/reporting/synthesis.js.map +1 -1
- package/dist/audit/reporting/workBlocks.d.ts +14 -3
- package/dist/audit/reporting/workBlocks.d.ts.map +1 -1
- package/dist/audit/reporting/workBlocks.js +163 -73
- package/dist/audit/reporting/workBlocks.js.map +1 -1
- package/dist/audit/types/activeDispatch.d.ts +8 -7
- package/dist/audit/types/activeDispatch.d.ts.map +1 -1
- package/dist/audit/types/auditState.d.ts +6 -6
- package/dist/audit/types/externalAnalyzer.d.ts +6 -6
- package/dist/audit/validation/auditResults.d.ts.map +1 -1
- package/dist/audit/validation/auditResults.js +6 -1
- package/dist/audit/validation/auditResults.js.map +1 -1
- package/dist/remediate/contractPipeline/phaseCut.d.ts +8 -0
- package/dist/remediate/contractPipeline/phaseCut.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/phaseCut.js +63 -0
- package/dist/remediate/contractPipeline/phaseCut.js.map +1 -1
- package/dist/remediate/contractPipeline/phaseCutArtifact.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/phaseCutArtifact.js +5 -3
- package/dist/remediate/contractPipeline/phaseCutArtifact.js.map +1 -1
- package/dist/remediate/phases/plan.d.ts.map +1 -1
- package/dist/remediate/phases/plan.js +35 -12
- package/dist/remediate/phases/plan.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts +6 -2
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +90 -6
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.js +4 -2
- package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
- package/dist/remediate/steps/dispatch/marshal.d.ts +6 -0
- package/dist/remediate/steps/dispatch/marshal.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/marshal.js +16 -17
- package/dist/remediate/steps/dispatch/marshal.js.map +1 -1
- package/dist/remediate/steps/dispatch/waveScheduling.d.ts +7 -15
- package/dist/remediate/steps/dispatch/waveScheduling.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/waveScheduling.js +26 -19
- package/dist/remediate/steps/dispatch/waveScheduling.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +5 -7
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +100 -82
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/rollingSession.d.ts +2 -1
- package/dist/remediate/steps/rollingSession.d.ts.map +1 -1
- package/dist/remediate/steps/rollingSession.js +14 -8
- package/dist/remediate/steps/rollingSession.js.map +1 -1
- package/dist/remediate/steps/sessionConfigLoad.d.ts +3 -2
- package/dist/remediate/steps/sessionConfigLoad.d.ts.map +1 -1
- package/dist/remediate/steps/sessionConfigLoad.js +4 -10
- package/dist/remediate/steps/sessionConfigLoad.js.map +1 -1
- package/dist/remediate/validation/contractPipeline.d.ts +1 -1
- package/dist/remediate/validation/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/validation/contractPipeline.js +7 -1
- package/dist/remediate/validation/contractPipeline.js.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.d.ts +8 -0
- package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.js +89 -0
- package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
- package/dist/shared/config/resolveSessionConfig.d.ts.map +1 -1
- package/dist/shared/config/resolveSessionConfig.js +6 -5
- package/dist/shared/config/resolveSessionConfig.js.map +1 -1
- package/dist/shared/decompose/workPartition.d.ts +66 -0
- package/dist/shared/decompose/workPartition.d.ts.map +1 -0
- package/dist/shared/decompose/workPartition.js +523 -0
- package/dist/shared/decompose/workPartition.js.map +1 -0
- package/dist/shared/dispatch/admissionLoop.d.ts +5 -44
- package/dist/shared/dispatch/admissionLoop.d.ts.map +1 -1
- package/dist/shared/dispatch/admissionLoop.js +20 -82
- package/dist/shared/dispatch/admissionLoop.js.map +1 -1
- package/dist/shared/dispatch/coordinator.d.ts +5 -5
- package/dist/shared/dispatch/coordinator.d.ts.map +1 -1
- package/dist/shared/dispatch/coordinator.js +12 -11
- package/dist/shared/dispatch/coordinator.js.map +1 -1
- package/dist/shared/dispatch/costRank.d.ts +5 -155
- package/dist/shared/dispatch/costRank.d.ts.map +1 -1
- package/dist/shared/dispatch/costRank.js +2 -179
- package/dist/shared/dispatch/costRank.js.map +1 -1
- package/dist/shared/dispatch/dispatchQuotaContract.d.ts +37 -38
- package/dist/shared/dispatch/dispatchQuotaContract.d.ts.map +1 -1
- package/dist/shared/dispatch/dispatchQuotaContract.js +0 -1
- package/dist/shared/dispatch/dispatchQuotaContract.js.map +1 -1
- package/dist/shared/dispatch/hybridDispatch.d.ts +6 -2
- package/dist/shared/dispatch/hybridDispatch.d.ts.map +1 -1
- package/dist/shared/dispatch/hybridDispatch.js +1 -1
- package/dist/shared/dispatch/hybridDispatch.js.map +1 -1
- package/dist/shared/dispatch/rollingDispatch.d.ts +4 -4
- package/dist/shared/dispatch/rollingDispatch.d.ts.map +1 -1
- package/dist/shared/dispatch/rollingDispatch.js +13 -11
- package/dist/shared/dispatch/rollingDispatch.js.map +1 -1
- package/dist/shared/dispatch/unifiedRolling.d.ts +2 -2
- package/dist/shared/dispatch/unifiedRolling.d.ts.map +1 -1
- package/dist/shared/dispatch/unifiedRolling.js +1 -1
- package/dist/shared/dispatch/unifiedRolling.js.map +1 -1
- package/dist/shared/engine/obligationEngine.d.ts +2 -2
- package/dist/shared/friction/stepBoundaryCapture.d.ts +5 -59
- package/dist/shared/friction/stepBoundaryCapture.d.ts.map +1 -1
- package/dist/shared/friction/stepBoundaryCapture.js +11 -107
- package/dist/shared/friction/stepBoundaryCapture.js.map +1 -1
- package/dist/shared/index.d.ts +13 -16
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +9 -13
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/io/stateDir.d.ts +2 -2
- package/dist/shared/io/stateDir.js +2 -2
- package/dist/shared/providers/auditorSources.d.ts +10 -84
- package/dist/shared/providers/auditorSources.d.ts.map +1 -1
- package/dist/shared/providers/auditorSources.js +42 -261
- package/dist/shared/providers/auditorSources.js.map +1 -1
- package/dist/shared/providers/dispatchExclusion.d.ts +22 -0
- package/dist/shared/providers/dispatchExclusion.d.ts.map +1 -0
- package/dist/shared/providers/dispatchExclusion.js +23 -0
- package/dist/shared/providers/dispatchExclusion.js.map +1 -0
- package/dist/shared/providers/identity.d.ts +2 -66
- package/dist/shared/providers/identity.d.ts.map +1 -1
- package/dist/shared/providers/identity.js +2 -72
- package/dist/shared/providers/identity.js.map +1 -1
- package/dist/shared/providers/providerFactory.d.ts +0 -3
- package/dist/shared/providers/providerFactory.d.ts.map +1 -1
- package/dist/shared/providers/providerFactory.js +5 -32
- package/dist/shared/providers/providerFactory.js.map +1 -1
- package/dist/shared/providers/providerPathGuard.d.ts +3 -3
- package/dist/shared/providers/providerPathGuard.js +7 -7
- package/dist/shared/providers/providerPathGuard.js.map +1 -1
- package/dist/shared/quota/apiPool.d.ts +19 -22
- package/dist/shared/quota/apiPool.d.ts.map +1 -1
- package/dist/shared/quota/apiPool.js +66 -47
- package/dist/shared/quota/apiPool.js.map +1 -1
- package/dist/shared/quota/capacity.d.ts +29 -24
- package/dist/shared/quota/capacity.d.ts.map +1 -1
- package/dist/shared/quota/capacity.js +30 -4
- package/dist/shared/quota/capacity.js.map +1 -1
- package/dist/shared/quota/codexHostConfig.d.ts +6 -13
- package/dist/shared/quota/codexHostConfig.d.ts.map +1 -1
- package/dist/shared/quota/codexHostConfig.js +8 -14
- package/dist/shared/quota/codexHostConfig.js.map +1 -1
- package/dist/shared/quota/fileLock.d.ts +7 -0
- package/dist/shared/quota/fileLock.d.ts.map +1 -1
- package/dist/shared/quota/fileLock.js +32 -4
- package/dist/shared/quota/fileLock.js.map +1 -1
- package/dist/shared/quota/hostLimits.d.ts +1 -1
- package/dist/shared/quota/hostLimits.d.ts.map +1 -1
- package/dist/shared/quota/hostLimits.js +8 -11
- package/dist/shared/quota/hostLimits.js.map +1 -1
- package/dist/shared/quota/hostPool.d.ts +1 -1
- package/dist/shared/quota/hostPool.js +2 -2
- package/dist/shared/quota/hostPool.js.map +1 -1
- package/dist/shared/quota/limits.d.ts.map +1 -1
- package/dist/shared/quota/limits.js +30 -26
- package/dist/shared/quota/limits.js.map +1 -1
- package/dist/shared/quota/scheduler.d.ts +2 -3
- package/dist/shared/quota/scheduler.d.ts.map +1 -1
- package/dist/shared/quota/scheduler.js +1 -1
- package/dist/shared/quota/scheduler.js.map +1 -1
- package/dist/shared/quota/types.d.ts +10 -10
- package/dist/shared/quota/types.d.ts.map +1 -1
- package/dist/shared/quota/types.js +4 -7
- package/dist/shared/quota/types.js.map +1 -1
- package/dist/shared/repair/brokeredDispatch.d.ts.map +1 -1
- package/dist/shared/repair/brokeredDispatch.js +3 -0
- package/dist/shared/repair/brokeredDispatch.js.map +1 -1
- package/dist/shared/reporting/auditDeliverable.d.ts.map +1 -1
- package/dist/shared/reporting/auditDeliverable.js +1 -0
- package/dist/shared/reporting/auditDeliverable.js.map +1 -1
- package/dist/shared/rolling/pausedState.d.ts +7 -7
- package/dist/shared/rolling/pausedState.js +3 -3
- package/dist/shared/tokens.d.ts +2 -5
- package/dist/shared/tokens.d.ts.map +1 -1
- package/dist/shared/tokens.js +15 -15
- package/dist/shared/tokens.js.map +1 -1
- package/dist/shared/types/auditorDescriptor.d.ts +3 -2
- package/dist/shared/types/auditorDescriptor.d.ts.map +1 -1
- package/dist/shared/types/auditorDescriptor.js +3 -2
- package/dist/shared/types/auditorDescriptor.js.map +1 -1
- package/dist/shared/types/finding.d.ts +80 -0
- package/dist/shared/types/finding.d.ts.map +1 -1
- package/dist/shared/types/finding.js +17 -0
- package/dist/shared/types/finding.js.map +1 -1
- package/dist/shared/types/sessionConfig.d.ts +5 -5
- package/dist/shared/types/sessionConfig.js +1 -1
- package/dist/shared/validation/findingsReport.d.ts.map +1 -1
- package/dist/shared/validation/findingsReport.js +3 -0
- package/dist/shared/validation/findingsReport.js.map +1 -1
- package/docs/audit-pkg/development.md +3 -2
- package/docs/audit-pkg/operator-guide.md +13 -106
- package/docs/audit-pkg/product.md +8 -7
- package/docs/audit-pkg/release.md +38 -29
- package/opencode.json +2 -2
- package/package.json +7 -4
- package/skills/audit-code/SKILL.md +2 -1
- package/skills/audit-code/audit-code.prompt.md +4 -3
- package/skills/remediate-code/remediate-code.prompt.md +2 -1
- package/dist/audit/cli/providerConfirmationStep.d.ts +0 -106
- package/dist/audit/cli/providerConfirmationStep.d.ts.map +0 -1
- package/dist/audit/cli/providerConfirmationStep.js +0 -396
- package/dist/audit/cli/providerConfirmationStep.js.map +0 -1
- package/dist/audit/orchestrator/providerConfirmation.d.ts +0 -25
- package/dist/audit/orchestrator/providerConfirmation.d.ts.map +0 -1
- package/dist/audit/orchestrator/providerConfirmation.js +0 -60
- package/dist/audit/orchestrator/providerConfirmation.js.map +0 -1
- package/dist/shared/providers/providerConfirmation.d.ts +0 -141
- package/dist/shared/providers/providerConfirmation.d.ts.map +0 -1
- package/dist/shared/providers/providerConfirmation.js +0 -420
- package/dist/shared/providers/providerConfirmation.js.map +0 -1
- package/dist/shared/providers/proxyCatalog.d.ts +0 -158
- package/dist/shared/providers/proxyCatalog.d.ts.map +0 -1
- package/dist/shared/providers/proxyCatalog.js +0 -658
- package/dist/shared/providers/proxyCatalog.js.map +0 -1
- package/dist/shared/providers/sharedProviderConfirmation.d.ts +0 -802
- package/dist/shared/providers/sharedProviderConfirmation.d.ts.map +0 -1
- package/dist/shared/providers/sharedProviderConfirmation.js +0 -1865
- package/dist/shared/providers/sharedProviderConfirmation.js.map +0 -1
- package/dist/shared/types/providerConfirmation.d.ts +0 -334
- package/dist/shared/types/providerConfirmation.d.ts.map +0 -1
- package/dist/shared/types/providerConfirmation.js +0 -35
- package/dist/shared/types/providerConfirmation.js.map +0 -1
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { writeJsonFile } from "../io/json.js";
|
|
5
|
-
import { resolveAuditCodeStateDir } from "../io/stateDir.js";
|
|
6
|
-
import { validateSessionConfig } from "../validation/sessionConfig.js";
|
|
7
|
-
/**
|
|
8
|
-
* The POPULATE cache for the proxy lane: discovered via `GET <proxy>/v1/models` +
|
|
9
|
-
* `GET <proxy>/model/info` (OpenAI-compatible surfaces) expanded into ready-to-fold
|
|
10
|
-
* `claude-worker` {@link DispatchableSource}s, written once per populate (Gate-0 build
|
|
11
|
-
* / explicit refresh) and READ by resolve — never fetched mid-resolve.
|
|
12
|
-
*
|
|
13
|
-
* Named `catalog-cache.json`, NOT the `catalog-<auditor-id>.json` the reserved-name
|
|
14
|
-
* comment at `auditorSources.ts` anticipates: populate/resolve run on the AMBIENT
|
|
15
|
-
* path, where no auditor id exists to key on. The cache is machine-level like the
|
|
16
|
-
* declaration beside it — and it is a CACHE of live discovery state, not resolved
|
|
17
|
-
* per-auditor capability, so an auditor-id key would assert an isolation the data
|
|
18
|
-
* doesn't have. Per-auditor never-inherit still holds: every resolve re-proves proxy
|
|
19
|
-
* REACH itself; the cache only supplies the expansion.
|
|
20
|
-
*/
|
|
21
|
-
export const PROXY_CATALOG_FILENAME = "catalog-cache.json";
|
|
22
|
-
/** Top-K models expanded per backend provider when the operator declares no `top_k`. */
|
|
23
|
-
export const DEFAULT_PROXY_TOP_K = 3;
|
|
24
|
-
/**
|
|
25
|
-
* USD-per-token → USD-per-million-tokens conversion factor. LiteLLM
|
|
26
|
-
* `/model/info` advertises `input_cost_per_token` / `output_cost_per_token`
|
|
27
|
-
* per TOKEN; the `cost_per_mtok` source field is per MILLION tokens
|
|
28
|
-
* (INV-SCC-03 unit discipline).
|
|
29
|
-
*/
|
|
30
|
-
const PER_TOKEN_TO_PER_MTOK = 1_000_000;
|
|
31
|
-
/** Timeout (ms) for populate-time model verification probes. */
|
|
32
|
-
export const POPULATE_PROBE_TIMEOUT_MS = 3000;
|
|
33
|
-
/** Concurrency limit for populate-time model verification probes. */
|
|
34
|
-
export const POPULATE_PROBE_CONCURRENCY = 4;
|
|
35
|
-
/**
|
|
36
|
-
* A same-endpoint cache younger than this skips the registry fetch + probes
|
|
37
|
-
* entirely (see the freshness short-circuit in {@link populateProxyCatalog}).
|
|
38
|
-
* Refresh THROTTLE — how often we are willing to PAY for a refresh, which is a
|
|
39
|
-
* different question from how long an unrefreshed cache may still be believed
|
|
40
|
-
* ({@link PROXY_CATALOG_MAX_READ_AGE_MS}). The two thresholds share one age
|
|
41
|
-
* derivation and nothing else.
|
|
42
|
-
*/
|
|
43
|
-
export const POPULATE_CACHE_FRESH_TTL_MS = 10 * 60_000;
|
|
44
|
-
/**
|
|
45
|
-
* Past this age a cache is returned STALE by {@link readProxyCatalog} — the
|
|
46
|
-
* expansion still travels, with a verdict attached, so the caller decides rather
|
|
47
|
-
* than consuming an unbounded-age cache blind.
|
|
48
|
-
*
|
|
49
|
-
* A day: comfortably longer than any single run (a mid-run flip to stale would be
|
|
50
|
-
* churn, not signal), and short enough that a roster the operator changed — a model
|
|
51
|
-
* added to or dropped from the proxy config — cannot go unnoticed indefinitely.
|
|
52
|
-
* The cache is machine-global and tool-written, so being wrong here costs one
|
|
53
|
-
* repopulate, never data.
|
|
54
|
-
*/
|
|
55
|
-
export const PROXY_CATALOG_MAX_READ_AGE_MS = 24 * 60 * 60_000;
|
|
56
|
-
/** Resolve the populate-cache path for this machine (state dir via `io/stateDir.ts`). */
|
|
57
|
-
export function resolveProxyCatalogPath(homeDir) {
|
|
58
|
-
return join(resolveAuditCodeStateDir(homeDir), PROXY_CATALOG_FILENAME);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Shape version of the on-disk cache. Bump whenever a written entry's meaning changes,
|
|
62
|
-
* so a cache written by older code degrades to "absent" rather than being read under
|
|
63
|
-
* new semantics. The cache is tool-written and fully regenerable, so degrading is free —
|
|
64
|
-
* `readProxyCatalog` returns null, and `populateProxyCatalogIfMissing` then repopulates.
|
|
65
|
-
*
|
|
66
|
-
* v2: entries no longer carry a tool-stamped `id`. A v1 entry does, and since `id` now
|
|
67
|
-
* outranks the `service` derivation in `dispatchableSourceId`, reading one under v2
|
|
68
|
-
* semantics would key a proxied lane to `claude-worker:<service>/<model>` instead of
|
|
69
|
-
* `<service>/<model>` — silently splitting it from the direct lane to the same backend
|
|
70
|
-
* and reopening the double-grant boundary.
|
|
71
|
-
*/
|
|
72
|
-
export const PROXY_CATALOG_VERSION = 2;
|
|
73
|
-
function finiteNonNegative(value) {
|
|
74
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
75
|
-
? value
|
|
76
|
-
: undefined;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Edge adapter: proxy `/model/info` response shape → neutral ModelAdvert contract.
|
|
80
|
-
* Maps from proxy-specific field names to a generic contract.
|
|
81
|
-
*
|
|
82
|
-
* Returns:
|
|
83
|
-
* - { advert, filtered: false | undefined } for valid models
|
|
84
|
-
* - { filtered: true } for models filtered by eligibility rules
|
|
85
|
-
* - undefined for invalid data (missing modelName/model_info)
|
|
86
|
-
*/
|
|
87
|
-
function adaptProxyModelInfo(modelInfo) {
|
|
88
|
-
const modelName = modelInfo.model_name;
|
|
89
|
-
if (typeof modelName !== "string" || modelName.trim().length === 0)
|
|
90
|
-
return undefined;
|
|
91
|
-
const info = modelInfo.model_info;
|
|
92
|
-
if (info === null || typeof info !== "object")
|
|
93
|
-
return undefined;
|
|
94
|
-
const infoObj = info;
|
|
95
|
-
const proxyProvider = infoObj.litellm_provider;
|
|
96
|
-
const provider = typeof proxyProvider === "string" ? proxyProvider : undefined;
|
|
97
|
-
const mode = infoObj.mode;
|
|
98
|
-
const supportsToolCalls = infoObj.supports_tool_calls;
|
|
99
|
-
// Check eligibility: mode must be "chat" or absent (unknown ≠ incapable).
|
|
100
|
-
// supports_tool_calls false → skip; absent or true → keep.
|
|
101
|
-
if (typeof mode === "string" && mode !== "chat")
|
|
102
|
-
return { filtered: true };
|
|
103
|
-
if (supportsToolCalls === false)
|
|
104
|
-
return { filtered: true };
|
|
105
|
-
return {
|
|
106
|
-
advert: {
|
|
107
|
-
alias: modelName.trim(),
|
|
108
|
-
...(provider !== undefined ? { provider } : {}),
|
|
109
|
-
context_tokens: finiteNonNegative(infoObj.max_input_tokens),
|
|
110
|
-
input_cost_per_token: finiteNonNegative(infoObj.input_cost_per_token),
|
|
111
|
-
output_cost_per_token: finiteNonNegative(infoObj.output_cost_per_token),
|
|
112
|
-
mode: typeof mode === "string" ? mode : undefined,
|
|
113
|
-
supports_tool_calls: typeof supportsToolCalls === "boolean" ? supportsToolCalls : undefined,
|
|
114
|
-
// Operator-declared rank via advert custom key (consumed as-is when present).
|
|
115
|
-
declared_rank: finiteNonNegative(infoObj.capability_rank),
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Best-effort "higher = better" score for ranking. Uses a flat `score` field
|
|
121
|
-
* when present; absent/non-finite → null (unscored models rank last).
|
|
122
|
-
*/
|
|
123
|
-
function deriveScore(entry) {
|
|
124
|
-
if (typeof entry.score === "number" && Number.isFinite(entry.score)) {
|
|
125
|
-
return entry.score;
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Tolerantly extract context-window field from a registry entry or its nested
|
|
131
|
-
* `capability` block. Checks for `context_length`, `context_tokens`, `max_context`,
|
|
132
|
-
* `context_window` (first finite positive number wins).
|
|
133
|
-
*/
|
|
134
|
-
function deriveContextTokens(entry) {
|
|
135
|
-
for (const key of ["context_length", "context_tokens", "max_context", "context_window"]) {
|
|
136
|
-
const value = finiteNonNegative(entry[key]);
|
|
137
|
-
if (value !== undefined)
|
|
138
|
-
return value;
|
|
139
|
-
}
|
|
140
|
-
const capability = entry.capability;
|
|
141
|
-
if (capability !== null && typeof capability === "object") {
|
|
142
|
-
for (const key of ["context_length", "context_tokens", "max_context", "context_window"]) {
|
|
143
|
-
const value = finiteNonNegative(capability[key]);
|
|
144
|
-
if (value !== undefined)
|
|
145
|
-
return value;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Discover models via the neutral proxy contract: `GET /v1/models` (required baseline)
|
|
152
|
-
* returns the alias roster; `GET /model/info` (optional enrichment) provides per-model
|
|
153
|
-
* metadata. Tolerant at every step: missing/unparseable/malformed → graceful degradation.
|
|
154
|
-
* Eligibility filters remove models from the pool entirely (mode != 'chat' or
|
|
155
|
-
* supports_tool_calls === false).
|
|
156
|
-
*
|
|
157
|
-
* Returns { adverts, filtered } where adverts is a map of alias → ModelAdvert,
|
|
158
|
-
* and filtered is a set of aliases that failed the eligibility filter.
|
|
159
|
-
*/
|
|
160
|
-
async function discoverModelAdverts(endpoint, fetchImpl, authHeader) {
|
|
161
|
-
const adverts = new Map();
|
|
162
|
-
const filtered = new Set();
|
|
163
|
-
try {
|
|
164
|
-
const headers = { "content-type": "application/json" };
|
|
165
|
-
if (authHeader)
|
|
166
|
-
headers.authorization = authHeader;
|
|
167
|
-
// Try /model/info first (richer advert); tolerate absent/unparseable
|
|
168
|
-
try {
|
|
169
|
-
const response = await fetchImpl(`${endpoint}/model/info`, { headers });
|
|
170
|
-
if (response.ok) {
|
|
171
|
-
const payload = await response.json();
|
|
172
|
-
// Target the current array form: {data: [...]}
|
|
173
|
-
if (payload !== null && typeof payload === "object") {
|
|
174
|
-
const data = payload.data;
|
|
175
|
-
if (Array.isArray(data)) {
|
|
176
|
-
for (const raw of data) {
|
|
177
|
-
if (raw !== null && typeof raw === "object") {
|
|
178
|
-
const result = adaptProxyModelInfo(raw);
|
|
179
|
-
if (result) {
|
|
180
|
-
if (result.filtered) {
|
|
181
|
-
// Track which aliases failed the eligibility filter
|
|
182
|
-
const modelName = raw.model_name;
|
|
183
|
-
if (typeof modelName === "string" && modelName.trim().length > 0) {
|
|
184
|
-
filtered.add(modelName.trim());
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else if (result.advert) {
|
|
188
|
-
// Successful adaptation
|
|
189
|
-
adverts.set(result.advert.alias, result.advert);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// If result is undefined, it's invalid data (not eligibility filter)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// /model/info absent/error → degrade to roster-only (no enrichment)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
// No adverts → degrade to roster-only (carrier of aliases only)
|
|
205
|
-
}
|
|
206
|
-
return { adverts, filtered };
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Discover the model roster via the neutral proxy contract: `GET /v1/models`
|
|
210
|
-
* (OpenAI-compatible list). Returns { aliases, error? }; absent/unparseable → empty aliases + reason.
|
|
211
|
-
*/
|
|
212
|
-
async function discoverModelRoster(endpoint, fetchImpl, authHeader) {
|
|
213
|
-
try {
|
|
214
|
-
const headers = { "content-type": "application/json" };
|
|
215
|
-
if (authHeader)
|
|
216
|
-
headers.authorization = authHeader;
|
|
217
|
-
const response = await fetchImpl(`${endpoint}/v1/models`, { headers });
|
|
218
|
-
if (!response.ok) {
|
|
219
|
-
return {
|
|
220
|
-
aliases: [],
|
|
221
|
-
error: `HTTP ${response.status}`,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
const payload = await response.json();
|
|
225
|
-
if (payload === null || typeof payload !== "object")
|
|
226
|
-
return { aliases: [] };
|
|
227
|
-
const data = payload.data;
|
|
228
|
-
if (!Array.isArray(data))
|
|
229
|
-
return { aliases: [] };
|
|
230
|
-
const aliases = [];
|
|
231
|
-
for (const entry of data) {
|
|
232
|
-
if (entry !== null && typeof entry === "object") {
|
|
233
|
-
const id = entry.id;
|
|
234
|
-
if (typeof id === "string" && id.trim().length > 0) {
|
|
235
|
-
aliases.push(id.trim());
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return { aliases };
|
|
240
|
-
}
|
|
241
|
-
catch (err) {
|
|
242
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
243
|
-
return { aliases: [], error: reason };
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Expand discovered models into `claude-worker` sources: per backend provider,
|
|
248
|
-
* the top-K models by best-effort score (higher = better; unscored last).
|
|
249
|
-
* Shape per the plan's identity section: the transport never enters the identity —
|
|
250
|
-
* `service` + `model` (alias) key quota; `endpoint` is the proxy url the
|
|
251
|
-
* launch transport fronts the spawn with; `worker_kind` is `agentic` by definition.
|
|
252
|
-
*
|
|
253
|
-
* Provider derivation: advert `provider` (from /model/info) > slash-prefix of alias
|
|
254
|
-
* (e.g., `anthropic/claude-*` → `anthropic`) > default shared `"proxy"` bucket
|
|
255
|
-
* (coarse pool identity at degradation rung, revisitable per owner decision).
|
|
256
|
-
*/
|
|
257
|
-
function expandSources(discovered, options) {
|
|
258
|
-
const byProvider = new Map();
|
|
259
|
-
// Dedup by (provider, alias): first row wins.
|
|
260
|
-
const seen = new Set();
|
|
261
|
-
for (const model of discovered) {
|
|
262
|
-
const identity = `${model.provider}/${model.alias}`;
|
|
263
|
-
if (seen.has(identity))
|
|
264
|
-
continue;
|
|
265
|
-
seen.add(identity);
|
|
266
|
-
const bucket = byProvider.get(model.provider) ?? [];
|
|
267
|
-
bucket.push(model);
|
|
268
|
-
byProvider.set(model.provider, bucket);
|
|
269
|
-
}
|
|
270
|
-
const sources = [];
|
|
271
|
-
const dropped = [];
|
|
272
|
-
// Stable, content-derived order (provider, then score desc, then alias) so
|
|
273
|
-
// re-populate over identical state emits byte-identical sources.
|
|
274
|
-
for (const provider of [...byProvider.keys()].sort()) {
|
|
275
|
-
const bucket = byProvider.get(provider);
|
|
276
|
-
const ordered = bucket.sort((a, b) => (b.score ?? Number.NEGATIVE_INFINITY) - (a.score ?? Number.NEGATIVE_INFINITY) ||
|
|
277
|
-
a.alias.localeCompare(b.alias));
|
|
278
|
-
const ranked = ordered.slice(0, options.topK);
|
|
279
|
-
// Truncation is never silent. With every `score` null the comparator's first
|
|
280
|
-
// term is NaN (-Infinity - -Infinity), so the sort degenerates to
|
|
281
|
-
// `localeCompare` and `top_k` keeps the ALPHABETICALLY-first models — which
|
|
282
|
-
// once kept a *flash* model and dropped every frontier one. The order still
|
|
283
|
-
// has to be deterministic (a content-derived array order is an artifact
|
|
284
|
-
// invariant), so the fix is not a different order: it is that every model
|
|
285
|
-
// top_k discards reaches the operator through the same `dropped[]` channel
|
|
286
|
-
// as an unreachable model, and says WHICH basis chose it.
|
|
287
|
-
const rankedCount = ordered.filter((m) => m.score !== null).length;
|
|
288
|
-
const basis = rankedCount === 0
|
|
289
|
-
? `no model in this backend advertises a rank, so the cut is ALPHABETICAL, not by capability`
|
|
290
|
-
: rankedCount === ordered.length
|
|
291
|
-
? `cut by advertised rank`
|
|
292
|
-
: `only ${rankedCount} of ${ordered.length} models advertise a rank; unranked models sort last and were cut alphabetically among themselves`;
|
|
293
|
-
for (const model of ordered.slice(options.topK)) {
|
|
294
|
-
dropped.push({
|
|
295
|
-
id: `claude-worker:${provider}/${model.alias}`,
|
|
296
|
-
reason: `top_k=${options.topK} kept ${ranked.length} of ${ordered.length} models for backend ` +
|
|
297
|
-
`"${provider}" — ${basis}. Raise proxy.top_k, or advertise a rank, to keep it.`,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
for (const model of ranked) {
|
|
301
|
-
// Cost precedence: operator-declared (free-to-operator axis) > advert price
|
|
302
|
-
// > absent (falls through to models.dev catalog / tier downstream).
|
|
303
|
-
const cost = options.costPerMtok ?? model.costPerMtok;
|
|
304
|
-
sources.push({
|
|
305
|
-
// No `id` stamped, deliberately. `id` is an operator OVERRIDE that outranks
|
|
306
|
-
// derivation (spec/backend-identity-axes.md); a tool-stamped transport-shaped
|
|
307
|
-
// id would claim that override and re-split the very identity `service` exists
|
|
308
|
-
// to merge — a proxied lane and a direct lane to the same service+model would
|
|
309
|
-
// key to two CapacityPools, reopening the double-grant boundary. Leaving it
|
|
310
|
-
// absent lets `dispatchableSourceId` derive `service[#account]/model`, which
|
|
311
|
-
// dedups the two lanes by construction.
|
|
312
|
-
transport: "claude-worker",
|
|
313
|
-
endpoint: options.endpoint,
|
|
314
|
-
service: provider,
|
|
315
|
-
model: model.alias, // Alias VERBATIM — the proxy's routing key
|
|
316
|
-
worker_kind: "agentic",
|
|
317
|
-
...(options.apiKeyEnv !== undefined ? { api_key_env: options.apiKeyEnv } : {}),
|
|
318
|
-
...(cost !== undefined ? { cost_per_mtok: cost } : {}),
|
|
319
|
-
// Step C: per-model capability (raw rank, LOWER = better) rides the source →
|
|
320
|
-
// CapacityPool.declaredCapabilityRank → the admission capability floor.
|
|
321
|
-
...(model.capabilityRank !== undefined
|
|
322
|
-
? { capability_rank: model.capabilityRank }
|
|
323
|
-
: {}),
|
|
324
|
-
...(model.contextTokens !== undefined
|
|
325
|
-
? { quota: { context_tokens: model.contextTokens } }
|
|
326
|
-
: {}),
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return { sources, dropped };
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Probe a single model via Anthropic-compatible `/v1/messages` to verify it is
|
|
334
|
-
* actually reachable. Returns { dropped: true, reason } if the model should be
|
|
335
|
-
* excluded (404/unavailable), or { dropped: false } to keep it.
|
|
336
|
-
*/
|
|
337
|
-
async function probeModelViaMessages(alias, backendProvider, endpoint, fetchImpl, timeoutMs, authHeader) {
|
|
338
|
-
const controller = new AbortController();
|
|
339
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
340
|
-
try {
|
|
341
|
-
const headers = {
|
|
342
|
-
"anthropic-version": "2023-06-01",
|
|
343
|
-
"content-type": "application/json",
|
|
344
|
-
};
|
|
345
|
-
if (authHeader)
|
|
346
|
-
headers.authorization = authHeader;
|
|
347
|
-
const response = await fetchImpl(`${endpoint}/v1/messages`, {
|
|
348
|
-
method: "POST",
|
|
349
|
-
headers,
|
|
350
|
-
body: JSON.stringify({
|
|
351
|
-
model: alias, // Route on proxy alias, not backend namespace
|
|
352
|
-
max_tokens: 1,
|
|
353
|
-
messages: [{ role: "user", content: "hi" }],
|
|
354
|
-
}),
|
|
355
|
-
signal: controller.signal,
|
|
356
|
-
});
|
|
357
|
-
// Check for 404 or model-not-found patterns
|
|
358
|
-
if (response.status === 404) {
|
|
359
|
-
return { dropped: true, reason: "HTTP 404" };
|
|
360
|
-
}
|
|
361
|
-
if (response.status >= 400) {
|
|
362
|
-
let bodyText = "";
|
|
363
|
-
try {
|
|
364
|
-
bodyText = await response.text();
|
|
365
|
-
}
|
|
366
|
-
catch {
|
|
367
|
-
// If body read fails, just use status code
|
|
368
|
-
}
|
|
369
|
-
const patterns = [/model_not_found/i, /\bmay not exist\b/i, /no such model/i];
|
|
370
|
-
if (patterns.some((p) => p.test(bodyText))) {
|
|
371
|
-
return { dropped: true, reason: `HTTP ${response.status} (model unavailable)` };
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
// 200, 401, 429, 5xx, etc. → keep the model
|
|
375
|
-
return { dropped: false };
|
|
376
|
-
}
|
|
377
|
-
catch (error) {
|
|
378
|
-
// Transport failure (timeout, network error) → fail-open, keep the model
|
|
379
|
-
return { dropped: false };
|
|
380
|
-
}
|
|
381
|
-
finally {
|
|
382
|
-
clearTimeout(timeoutId);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Probe models with bounded concurrency using a simple worker pool.
|
|
387
|
-
*/
|
|
388
|
-
async function probeModelsWithConcurrency(aliases, backendProvider, endpoint, fetchImpl, timeoutMs, concurrency, authHeader) {
|
|
389
|
-
const results = [];
|
|
390
|
-
let index = 0;
|
|
391
|
-
const worker = async () => {
|
|
392
|
-
while (index < aliases.length) {
|
|
393
|
-
const currentIndex = index++;
|
|
394
|
-
const alias = aliases[currentIndex];
|
|
395
|
-
const probeResult = await probeModelViaMessages(alias, backendProvider, endpoint, fetchImpl, timeoutMs, authHeader);
|
|
396
|
-
results[currentIndex] = {
|
|
397
|
-
alias,
|
|
398
|
-
dropped: probeResult.dropped,
|
|
399
|
-
reason: probeResult.reason,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
const workers = Array(Math.min(concurrency, aliases.length))
|
|
404
|
-
.fill(null)
|
|
405
|
-
.map(() => worker());
|
|
406
|
-
await Promise.all(workers);
|
|
407
|
-
// results is index-assigned, so already in order.
|
|
408
|
-
return results;
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* POPULATE: discover and expand models from an OpenAI-compatible proxy
|
|
412
|
-
* (`GET <endpoint>/v1/models` + `GET <endpoint>/model/info`) into
|
|
413
|
-
* ready-to-fold `claude-worker` sources, then write to the machine-level cache.
|
|
414
|
-
* Network-bound and cacheable — runs at Gate-0 build / explicit refresh, NEVER
|
|
415
|
-
* inside `resolveAmbientSources` (which only READS via {@link readProxyCatalog}).
|
|
416
|
-
* Never throws: a failed fetch returns `{written:false, reason}` and leaves any
|
|
417
|
-
* prior cache untouched; an empty discovery WRITES an empty expansion (fresh
|
|
418
|
-
* knowledge — resolve reports the lane unexpanded with a reason).
|
|
419
|
-
*/
|
|
420
|
-
export async function populateProxyCatalog(options) {
|
|
421
|
-
const endpoint = options.endpoint.replace(/\/+$/u, "");
|
|
422
|
-
const doFetch = options.fetchImpl ?? fetch;
|
|
423
|
-
// Build auth header when api_key_env is declared and the env var is set.
|
|
424
|
-
let authHeader;
|
|
425
|
-
if (options.apiKeyEnv) {
|
|
426
|
-
const keyValue = process.env[options.apiKeyEnv];
|
|
427
|
-
if (keyValue?.trim()) {
|
|
428
|
-
authHeader = `Bearer ${keyValue}`;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
// Freshness short-circuit: populate carries live per-model probes — real
|
|
432
|
-
// `/v1/messages` POSTs through the proxy that cost seconds and burn quota.
|
|
433
|
-
// A same-endpoint cache younger than the TTL answers instead. This is a
|
|
434
|
-
// REFRESH throttle, a tighter window than the read-side staleness bound;
|
|
435
|
-
// the age itself comes from the ONE derivation in `readProxyCatalog` (on this
|
|
436
|
-
// call's clock), so the two thresholds can never disagree about how old a file is.
|
|
437
|
-
const cached = readProxyCatalog({ homeDir: options.homeDir, now: options.now });
|
|
438
|
-
if (cached &&
|
|
439
|
-
cached.endpoint === endpoint &&
|
|
440
|
-
cached.age_ms >= 0 &&
|
|
441
|
-
cached.age_ms < POPULATE_CACHE_FRESH_TTL_MS) {
|
|
442
|
-
return {
|
|
443
|
-
sources: cached.sources,
|
|
444
|
-
written: false,
|
|
445
|
-
reason: `cache is fresh (fetched ${Math.round(cached.age_ms / 1000)}s ago); refresh skipped.`,
|
|
446
|
-
dropped: [],
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
// Discover model roster (required baseline: /v1/models).
|
|
450
|
-
const { aliases, error: rosterError } = await discoverModelRoster(endpoint, doFetch, authHeader);
|
|
451
|
-
if (aliases.length === 0) {
|
|
452
|
-
return {
|
|
453
|
-
sources: [],
|
|
454
|
-
written: false,
|
|
455
|
-
reason: rosterError
|
|
456
|
-
? `GET ${endpoint}/v1/models failed: ${rosterError}`
|
|
457
|
-
: `GET ${endpoint}/v1/models returned no models or was unreachable.`,
|
|
458
|
-
dropped: [],
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
// Discover model adverts (optional enrichment: /model/info).
|
|
462
|
-
const { adverts, filtered } = await discoverModelAdverts(endpoint, doFetch, authHeader);
|
|
463
|
-
// Build discovered models: join roster + adverts, deriving provider.
|
|
464
|
-
const discovered = [];
|
|
465
|
-
for (const alias of aliases) {
|
|
466
|
-
// Skip models that failed the eligibility filter
|
|
467
|
-
if (filtered.has(alias))
|
|
468
|
-
continue;
|
|
469
|
-
const advert = adverts.get(alias);
|
|
470
|
-
// Provider derivation: advert provider > slash-prefix > default "proxy" bucket.
|
|
471
|
-
let provider;
|
|
472
|
-
if (advert?.provider) {
|
|
473
|
-
provider = advert.provider;
|
|
474
|
-
}
|
|
475
|
-
else if (alias.includes("/")) {
|
|
476
|
-
provider = alias.split("/")[0];
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
provider = "proxy";
|
|
480
|
-
}
|
|
481
|
-
// Cost blend: mean of input/output when both present; otherwise the one
|
|
482
|
-
// present. UNIT DISCIPLINE (INV-SCC-03 / COR-6c1ad0ba): the advert fields
|
|
483
|
-
// (`input_cost_per_token` / `output_cost_per_token`, LiteLLM /model/info
|
|
484
|
-
// convention) are USD per TOKEN; `cost_per_mtok`'s contract is USD per
|
|
485
|
-
// MILLION tokens, so the blend converts ×1e6. Writing the per-token value
|
|
486
|
-
// straight through underprices every proxied lane 1,000,000x and lets it
|
|
487
|
-
// incorrectly dominate cost-first routing.
|
|
488
|
-
const availableCosts = [];
|
|
489
|
-
if (advert?.input_cost_per_token !== undefined) {
|
|
490
|
-
availableCosts.push(advert.input_cost_per_token * PER_TOKEN_TO_PER_MTOK);
|
|
491
|
-
}
|
|
492
|
-
if (advert?.output_cost_per_token !== undefined) {
|
|
493
|
-
availableCosts.push(advert.output_cost_per_token * PER_TOKEN_TO_PER_MTOK);
|
|
494
|
-
}
|
|
495
|
-
let costPerMtok;
|
|
496
|
-
if (availableCosts.length === 2) {
|
|
497
|
-
costPerMtok = (availableCosts[0] + availableCosts[1]) / 2;
|
|
498
|
-
}
|
|
499
|
-
else if (availableCosts.length === 1) {
|
|
500
|
-
costPerMtok = availableCosts[0];
|
|
501
|
-
}
|
|
502
|
-
discovered.push({
|
|
503
|
-
alias,
|
|
504
|
-
provider,
|
|
505
|
-
score: advert?.declared_rank !== undefined ? -advert.declared_rank : null,
|
|
506
|
-
capabilityRank: advert?.declared_rank,
|
|
507
|
-
costPerMtok,
|
|
508
|
-
contextTokens: advert?.context_tokens,
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
// Probe to verify models are reachable and drop 404s.
|
|
512
|
-
// Group by provider for parallel probing per provider.
|
|
513
|
-
const byProvider = new Map();
|
|
514
|
-
for (const model of discovered) {
|
|
515
|
-
const bucket = byProvider.get(model.provider) ?? [];
|
|
516
|
-
bucket.push(model.alias);
|
|
517
|
-
byProvider.set(model.provider, bucket);
|
|
518
|
-
}
|
|
519
|
-
const droppedAliases = new Set();
|
|
520
|
-
const dropped = [];
|
|
521
|
-
for (const [provider, providerAliases] of byProvider) {
|
|
522
|
-
const probeResults = await probeModelsWithConcurrency(providerAliases, provider, endpoint, doFetch, POPULATE_PROBE_TIMEOUT_MS, POPULATE_PROBE_CONCURRENCY, authHeader);
|
|
523
|
-
for (const result of probeResults) {
|
|
524
|
-
if (result.dropped) {
|
|
525
|
-
droppedAliases.add(result.alias);
|
|
526
|
-
dropped.push({
|
|
527
|
-
id: `claude-worker:${provider}/${result.alias}`,
|
|
528
|
-
reason: result.reason ?? "model unavailable",
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
// Filter out dropped models and expand sources.
|
|
534
|
-
const toExpand = discovered.filter((m) => !droppedAliases.has(m.alias));
|
|
535
|
-
const expanded = expandSources(toExpand, {
|
|
536
|
-
endpoint,
|
|
537
|
-
topK: options.topK ?? DEFAULT_PROXY_TOP_K,
|
|
538
|
-
costPerMtok: options.costPerMtok,
|
|
539
|
-
apiKeyEnv: options.apiKeyEnv,
|
|
540
|
-
});
|
|
541
|
-
const sources = expanded.sources;
|
|
542
|
-
// top_k truncation joins the unreachable-model drops in the ONE operator-facing
|
|
543
|
-
// channel, so "the frontier model isn't there" always carries its reason.
|
|
544
|
-
dropped.push(...expanded.dropped);
|
|
545
|
-
const catalog = {
|
|
546
|
-
version: PROXY_CATALOG_VERSION,
|
|
547
|
-
fetched_at: (options.now?.() ?? new Date()).toISOString(),
|
|
548
|
-
endpoint,
|
|
549
|
-
sources,
|
|
550
|
-
};
|
|
551
|
-
const path = resolveProxyCatalogPath(options.homeDir);
|
|
552
|
-
try {
|
|
553
|
-
await mkdir(dirname(path), { recursive: true });
|
|
554
|
-
await writeJsonFile(path, catalog);
|
|
555
|
-
}
|
|
556
|
-
catch (error) {
|
|
557
|
-
return {
|
|
558
|
-
sources,
|
|
559
|
-
written: false,
|
|
560
|
-
reason: `could not write ${path}: ${error instanceof Error ? error.message : String(error)}.`,
|
|
561
|
-
dropped,
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
return { sources, written: true, dropped };
|
|
565
|
-
}
|
|
566
|
-
function defaultReadCatalogFile(path) {
|
|
567
|
-
try {
|
|
568
|
-
return readFileSync(path, "utf8");
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
/** Human-readable magnitude for an age/skew, in the largest unit that still reads. */
|
|
575
|
-
function describeAgeMs(ms) {
|
|
576
|
-
const abs = Math.abs(ms);
|
|
577
|
-
if (abs < 90 * 60_000)
|
|
578
|
-
return `${Math.round(abs / 60_000)}m`;
|
|
579
|
-
if (abs < 48 * 3_600_000)
|
|
580
|
-
return `${(abs / 3_600_000).toFixed(1)}h`;
|
|
581
|
-
return `${(abs / 86_400_000).toFixed(1)}d`;
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* READ the populate cache, with the read-side age rule applied: every result carries
|
|
585
|
-
* `age_ms` and a `stale` verdict against {@link PROXY_CATALOG_MAX_READ_AGE_MS}, so an
|
|
586
|
-
* old cache reaches its caller labelled rather than indistinguishable from a fresh one.
|
|
587
|
-
* Degrades to `null` on absent / unparseable / structurally-invalid content — never
|
|
588
|
-
* throws (same bar as `readSourceDeclaration`: the resolve path must not be failable by
|
|
589
|
-
* a bad file). Cached sources are held to the shared source validator, so a hand-edited
|
|
590
|
-
* or version-skewed cache degrades to "no cache" rather than admitting a half-checked
|
|
591
|
-
* pool.
|
|
592
|
-
*/
|
|
593
|
-
export function readProxyCatalog(deps = {}) {
|
|
594
|
-
const path = resolveProxyCatalogPath(deps.homeDir);
|
|
595
|
-
const raw = (deps.readCatalogFile ?? defaultReadCatalogFile)(path);
|
|
596
|
-
if (raw === null)
|
|
597
|
-
return null;
|
|
598
|
-
let parsed;
|
|
599
|
-
try {
|
|
600
|
-
parsed = JSON.parse(raw);
|
|
601
|
-
}
|
|
602
|
-
catch {
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
const { version, fetched_at, endpoint, sources } = parsed;
|
|
609
|
-
// Degrade a foreign-shape cache to "absent" rather than reading it under current
|
|
610
|
-
// semantics. A pre-versioned (v1) file carries tool-stamped ids that would now
|
|
611
|
-
// outrank the service derivation and re-split a proxied lane from its direct twin.
|
|
612
|
-
// Regenerable by design — the caller repopulates.
|
|
613
|
-
if (version !== PROXY_CATALOG_VERSION)
|
|
614
|
-
return null;
|
|
615
|
-
if (typeof fetched_at !== "string" || typeof endpoint !== "string")
|
|
616
|
-
return null;
|
|
617
|
-
if (!Array.isArray(sources))
|
|
618
|
-
return null;
|
|
619
|
-
// An unparseable stamp is a corrupt file, not an old one: no honest age can be
|
|
620
|
-
// derived from it, and aging it from a substituted default would launder it into
|
|
621
|
-
// a fresh-looking cache. Refuse it the way every other invalid field is refused.
|
|
622
|
-
const fetchedMs = Date.parse(fetched_at);
|
|
623
|
-
if (!Number.isFinite(fetchedMs))
|
|
624
|
-
return null;
|
|
625
|
-
const issues = validateSessionConfig({ sources });
|
|
626
|
-
if (issues.some((issue) => issue.severity === "error"))
|
|
627
|
-
return null;
|
|
628
|
-
const ageMs = (deps.now?.() ?? new Date()).getTime() - fetchedMs;
|
|
629
|
-
const catalog = {
|
|
630
|
-
version,
|
|
631
|
-
fetched_at,
|
|
632
|
-
endpoint,
|
|
633
|
-
sources: sources,
|
|
634
|
-
age_ms: ageMs,
|
|
635
|
-
};
|
|
636
|
-
// A stamp ahead of our clock proves nothing about freshness — it is skew or a hand
|
|
637
|
-
// edit, and believing it would pin the cache fresh for as long as the stamp says.
|
|
638
|
-
// Not fresh and not refused: stale, with the anomaly named. Mirrors the populate
|
|
639
|
-
// short-circuit, which likewise refuses to count a future stamp as young.
|
|
640
|
-
if (ageMs < 0) {
|
|
641
|
-
return {
|
|
642
|
-
...catalog,
|
|
643
|
-
stale: true,
|
|
644
|
-
stale_reason: `populate cache is stamped ${describeAgeMs(ageMs)} in the future ("${fetched_at}") ` +
|
|
645
|
-
`relative to this machine's clock, so its age cannot be trusted — re-run the populate.`,
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
if (ageMs >= PROXY_CATALOG_MAX_READ_AGE_MS) {
|
|
649
|
-
return {
|
|
650
|
-
...catalog,
|
|
651
|
-
stale: true,
|
|
652
|
-
stale_reason: `populate cache was fetched ${describeAgeMs(ageMs)} ago (max ${describeAgeMs(PROXY_CATALOG_MAX_READ_AGE_MS)}), ` +
|
|
653
|
-
`so its expansion may no longer match the proxy's roster — re-run the populate.`,
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
return { ...catalog, stale: false };
|
|
657
|
-
}
|
|
658
|
-
//# sourceMappingURL=proxyCatalog.js.map
|