@geraldmaron/construct 1.1.1 → 1.2.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/README.md +29 -19
- package/bin/construct +311 -81
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +112 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +1 -1
- package/lib/auto-docs.mjs +87 -18
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +245 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +129 -15
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +598 -140
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +13 -11
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/worker.mjs +2 -2
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +4 -5
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +27 -97
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +84 -9
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +373 -92
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +7 -3
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +228 -7
- package/lib/service-manager.mjs +60 -8
- package/lib/setup.mjs +70 -7
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +28 -14
- package/personas/construct.md +2 -2
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +41 -28
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +7 -2
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +480 -0
- package/specialists/artifact-manifest.schema.json +53 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +8 -8
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/behavior-matrix.mjs — provider/model behavior matrix (construct-6zga.1.4).
|
|
3
|
+
*
|
|
4
|
+
* The baseline evidence producer for the ExecutionCapabilityProfile
|
|
5
|
+
* (construct-6zga.1.8). It records measured execution behavior keyed by a
|
|
6
|
+
* structural capability CLASS — hosted-direct, hosted-routed, local-constrained,
|
|
7
|
+
* local-capable, unknown — not by vendor name. The class is derived only from the
|
|
8
|
+
* configured provider's transport (a configuration fact) plus measured signals;
|
|
9
|
+
* capability VALUES (tool support, context behavior, usage fields) come only from
|
|
10
|
+
* evidence, never from a model-name regex. Every observation carries an evidence
|
|
11
|
+
* provenance (hermetic_fixture | live_probe | provider_metadata | operator_override)
|
|
12
|
+
* so the profile can declare where each field came from.
|
|
13
|
+
*
|
|
14
|
+
* Hermetic fixtures are the CI source of truth. Live probing is opt-in only: it
|
|
15
|
+
* requires explicit operator confirmation, is run-bounded, redacts metadata, and
|
|
16
|
+
* appends evidence to a separate log — it never ranks providers and never mutates
|
|
17
|
+
* the active capability record. Reference shape: schemas/provider-behavior-matrix.schema.json.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
|
|
21
|
+
export const BEHAVIOR_MATRIX_SCHEMA_VERSION = 1;
|
|
22
|
+
|
|
23
|
+
export const CAPABILITY_CLASSES = Object.freeze([
|
|
24
|
+
'hosted-direct',
|
|
25
|
+
'hosted-routed',
|
|
26
|
+
'local-constrained',
|
|
27
|
+
'local-capable',
|
|
28
|
+
'unknown',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const TRANSPORTS = Object.freeze(['direct', 'routed', 'local', 'unknown']);
|
|
32
|
+
|
|
33
|
+
export const EVIDENCE_SOURCES = Object.freeze([
|
|
34
|
+
'hermetic_fixture',
|
|
35
|
+
'live_probe',
|
|
36
|
+
'provider_metadata',
|
|
37
|
+
'operator_override',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const SYSTEM_ACCEPTANCE = Object.freeze(['accepted', 'rejected', 'ignored', 'unknown']);
|
|
41
|
+
const TOOL_PARSE_SHAPES = Object.freeze(['native_tool_use', 'openai_function', 'none', 'unknown']);
|
|
42
|
+
const TOOL_RESULT_SHAPES = Object.freeze(['tool_result_block', 'message_role_tool', 'none', 'unknown']);
|
|
43
|
+
const CANCEL_MECHANISMS = Object.freeze(['abort_signal', 'none', 'unknown']);
|
|
44
|
+
const CONTEXT_FAILURES = Object.freeze(['error', 'truncate', 'silent_drop', 'unknown']);
|
|
45
|
+
const FALLBACK_CLASSES = Object.freeze(['retryable', 'fatal', 'rate_limited', 'model_unavailable', 'unknown']);
|
|
46
|
+
|
|
47
|
+
const MAX_PROBE_RUNS = 5;
|
|
48
|
+
const SECRET_KEY_RE = /(api[_-]?key|secret|token|authorization|password|bearer)/i;
|
|
49
|
+
|
|
50
|
+
// Transport is the provider's structural topology, taken from the configured
|
|
51
|
+
// provider group (the catalog source of truth in lib/model-router.mjs), mirroring
|
|
52
|
+
// providerGroupId in lib/models/provider-poll.mjs. It is not a capability guess:
|
|
53
|
+
// the aggregator routes a model, a local runtime hosts it, anything else is a
|
|
54
|
+
// direct hosted API. Unknown groups stay 'unknown' rather than assuming direct.
|
|
55
|
+
|
|
56
|
+
const LOCAL_GROUPS = Object.freeze(new Set(['ollama', 'local']));
|
|
57
|
+
const DIRECT_GROUPS = Object.freeze(new Set(['anthropic', 'openai', 'github-copilot']));
|
|
58
|
+
|
|
59
|
+
export function transportForProviderGroup(groupId) {
|
|
60
|
+
const gid = String(groupId || '');
|
|
61
|
+
if (gid.startsWith('openrouter')) return 'routed';
|
|
62
|
+
if (LOCAL_GROUPS.has(gid)) return 'local';
|
|
63
|
+
if (DIRECT_GROUPS.has(gid)) return 'direct';
|
|
64
|
+
return 'unknown';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Class is structural for hosted transports; for local runtimes it splits on
|
|
68
|
+
// measured signals only. Unmeasured local models are 'unknown', never assumed
|
|
69
|
+
// capable — the conservative default the profile relies on.
|
|
70
|
+
|
|
71
|
+
export function classifyCapabilityClass({ transport, measured = {} } = {}) {
|
|
72
|
+
if (transport === 'direct') return 'hosted-direct';
|
|
73
|
+
if (transport === 'routed') return 'hosted-routed';
|
|
74
|
+
if (transport === 'local') {
|
|
75
|
+
if (measured.toolsKnown !== true || typeof measured.coherent !== 'boolean') return 'unknown';
|
|
76
|
+
return measured.tools === true && measured.coherent === true ? 'local-capable' : 'local-constrained';
|
|
77
|
+
}
|
|
78
|
+
return 'unknown';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function inEnum(values, value) {
|
|
82
|
+
return values.includes(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function defaultedString(value, allowed, fallback = 'unknown') {
|
|
86
|
+
return inEnum(allowed, value) ? value : fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function nonNegativeInt(value) {
|
|
90
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Normalize a raw measurement into a frozen, schema-shaped observation. Missing
|
|
95
|
+
* fields collapse to conservative 'unknown'/0 defaults rather than being invented.
|
|
96
|
+
*/
|
|
97
|
+
export function buildObservation(input = {}) {
|
|
98
|
+
const requestShape = input.requestShape || {};
|
|
99
|
+
const toolCall = input.toolCall || {};
|
|
100
|
+
const cancellation = input.cancellation || {};
|
|
101
|
+
const usage = input.usage || {};
|
|
102
|
+
const evidence = input.evidence || {};
|
|
103
|
+
|
|
104
|
+
const observation = {
|
|
105
|
+
capabilityClass: defaultedString(input.capabilityClass, CAPABILITY_CLASSES),
|
|
106
|
+
transport: defaultedString(input.transport, TRANSPORTS),
|
|
107
|
+
adapterProtocol: typeof input.adapterProtocol === 'string' && input.adapterProtocol
|
|
108
|
+
? input.adapterProtocol
|
|
109
|
+
: 'unknown',
|
|
110
|
+
responseModelId: typeof input.responseModelId === 'string' ? input.responseModelId : null,
|
|
111
|
+
requestShape: Object.freeze({
|
|
112
|
+
system: defaultedString(requestShape.system, SYSTEM_ACCEPTANCE),
|
|
113
|
+
toolSchemaCount: nonNegativeInt(requestShape.toolSchemaCount),
|
|
114
|
+
toolSchemaTokenEstimate: nonNegativeInt(requestShape.toolSchemaTokenEstimate),
|
|
115
|
+
}),
|
|
116
|
+
toolCall: Object.freeze({
|
|
117
|
+
supported: toolCall.supported === true,
|
|
118
|
+
parseShape: defaultedString(toolCall.parseShape, TOOL_PARSE_SHAPES),
|
|
119
|
+
resultShape: defaultedString(toolCall.resultShape, TOOL_RESULT_SHAPES),
|
|
120
|
+
}),
|
|
121
|
+
cancellation: Object.freeze({
|
|
122
|
+
supported: cancellation.supported === true,
|
|
123
|
+
mechanism: defaultedString(cancellation.mechanism, CANCEL_MECHANISMS),
|
|
124
|
+
}),
|
|
125
|
+
usage: Object.freeze({
|
|
126
|
+
fields: Array.isArray(usage.fields) ? Object.freeze(usage.fields.filter((f) => typeof f === 'string')) : Object.freeze([]),
|
|
127
|
+
cost: usage.cost === true,
|
|
128
|
+
}),
|
|
129
|
+
contextFailureBehavior: defaultedString(input.contextFailureBehavior, CONTEXT_FAILURES),
|
|
130
|
+
fallbackClassification: defaultedString(input.fallbackClassification, FALLBACK_CLASSES),
|
|
131
|
+
evidence: Object.freeze({
|
|
132
|
+
source: defaultedString(evidence.source, EVIDENCE_SOURCES, 'hermetic_fixture'),
|
|
133
|
+
capturedAt: typeof evidence.capturedAt === 'string' ? evidence.capturedAt : null,
|
|
134
|
+
redacted: evidence.redacted === true,
|
|
135
|
+
runs: Number.isInteger(evidence.runs) && evidence.runs >= 1 ? evidence.runs : 1,
|
|
136
|
+
distribution: evidence.distribution && typeof evidence.distribution === 'object' ? evidence.distribution : null,
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
return Object.freeze(observation);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateObservation(obs, index, errors) {
|
|
143
|
+
const at = `observations[${index}]`;
|
|
144
|
+
if (!obs || typeof obs !== 'object') {
|
|
145
|
+
errors.push(`${at}: not an object`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!inEnum(CAPABILITY_CLASSES, obs.capabilityClass)) errors.push(`${at}.capabilityClass invalid: ${obs.capabilityClass}`);
|
|
149
|
+
if (!inEnum(TRANSPORTS, obs.transport)) errors.push(`${at}.transport invalid: ${obs.transport}`);
|
|
150
|
+
if (typeof obs.adapterProtocol !== 'string' || !obs.adapterProtocol) errors.push(`${at}.adapterProtocol missing`);
|
|
151
|
+
const rs = obs.requestShape || {};
|
|
152
|
+
if (!inEnum(SYSTEM_ACCEPTANCE, rs.system)) errors.push(`${at}.requestShape.system invalid: ${rs.system}`);
|
|
153
|
+
if (!Number.isInteger(rs.toolSchemaCount) || rs.toolSchemaCount < 0) errors.push(`${at}.requestShape.toolSchemaCount invalid`);
|
|
154
|
+
if (!Number.isInteger(rs.toolSchemaTokenEstimate) || rs.toolSchemaTokenEstimate < 0) errors.push(`${at}.requestShape.toolSchemaTokenEstimate invalid`);
|
|
155
|
+
const tc = obs.toolCall || {};
|
|
156
|
+
if (typeof tc.supported !== 'boolean') errors.push(`${at}.toolCall.supported invalid`);
|
|
157
|
+
if (!inEnum(TOOL_PARSE_SHAPES, tc.parseShape)) errors.push(`${at}.toolCall.parseShape invalid: ${tc.parseShape}`);
|
|
158
|
+
if (!inEnum(TOOL_RESULT_SHAPES, tc.resultShape)) errors.push(`${at}.toolCall.resultShape invalid: ${tc.resultShape}`);
|
|
159
|
+
const cancel = obs.cancellation || {};
|
|
160
|
+
if (typeof cancel.supported !== 'boolean') errors.push(`${at}.cancellation.supported invalid`);
|
|
161
|
+
if (!inEnum(CANCEL_MECHANISMS, cancel.mechanism)) errors.push(`${at}.cancellation.mechanism invalid: ${cancel.mechanism}`);
|
|
162
|
+
const usage = obs.usage || {};
|
|
163
|
+
if (!Array.isArray(usage.fields)) errors.push(`${at}.usage.fields invalid`);
|
|
164
|
+
if (typeof usage.cost !== 'boolean') errors.push(`${at}.usage.cost invalid`);
|
|
165
|
+
if (!inEnum(CONTEXT_FAILURES, obs.contextFailureBehavior)) errors.push(`${at}.contextFailureBehavior invalid: ${obs.contextFailureBehavior}`);
|
|
166
|
+
if (!inEnum(FALLBACK_CLASSES, obs.fallbackClassification)) errors.push(`${at}.fallbackClassification invalid: ${obs.fallbackClassification}`);
|
|
167
|
+
const ev = obs.evidence || {};
|
|
168
|
+
if (!inEnum(EVIDENCE_SOURCES, ev.source)) errors.push(`${at}.evidence.source invalid: ${ev.source}`);
|
|
169
|
+
if (typeof ev.redacted !== 'boolean') errors.push(`${at}.evidence.redacted invalid`);
|
|
170
|
+
if (!Number.isInteger(ev.runs) || ev.runs < 1) errors.push(`${at}.evidence.runs invalid`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Hand-rolled validator (no ajv — Construct stays dependency-free at startup).
|
|
175
|
+
* Returns { valid, errors } against schemas/provider-behavior-matrix.schema.json.
|
|
176
|
+
*/
|
|
177
|
+
export function validateBehaviorMatrix(doc) {
|
|
178
|
+
const errors = [];
|
|
179
|
+
if (!doc || typeof doc !== 'object') return { valid: false, errors: ['document is not an object'] };
|
|
180
|
+
if (!Number.isInteger(doc.version) || doc.version < 1) errors.push('version must be an integer >= 1');
|
|
181
|
+
if (!Array.isArray(doc.observations)) {
|
|
182
|
+
errors.push('observations must be an array');
|
|
183
|
+
return { valid: false, errors };
|
|
184
|
+
}
|
|
185
|
+
doc.observations.forEach((obs, i) => validateObservation(obs, i, errors));
|
|
186
|
+
return { valid: errors.length === 0, errors };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function loadBehaviorMatrix(filePath) {
|
|
190
|
+
const doc = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
191
|
+
const { valid, errors } = validateBehaviorMatrix(doc);
|
|
192
|
+
if (!valid) throw new Error(`invalid behavior matrix ${filePath}: ${errors.join('; ')}`);
|
|
193
|
+
return doc;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Repeated probes of one target rarely agree on every field, so the matrix
|
|
197
|
+
// reports a distribution rather than a single verdict. Agreement is the share of
|
|
198
|
+
// runs at the modal class; deterministic means every run agreed.
|
|
199
|
+
|
|
200
|
+
export function summarizeProbeRuns(runs) {
|
|
201
|
+
const list = Array.isArray(runs) ? runs.filter(Boolean) : [];
|
|
202
|
+
if (!list.length) return { runs: 0, deterministic: true, classCounts: {}, modalClass: 'unknown', agreement: 0, toolSupportedRatio: 0 };
|
|
203
|
+
const classCounts = {};
|
|
204
|
+
let toolSupported = 0;
|
|
205
|
+
for (const obs of list) {
|
|
206
|
+
const cls = inEnum(CAPABILITY_CLASSES, obs.capabilityClass) ? obs.capabilityClass : 'unknown';
|
|
207
|
+
classCounts[cls] = (classCounts[cls] || 0) + 1;
|
|
208
|
+
if (obs.toolCall?.supported === true) toolSupported += 1;
|
|
209
|
+
}
|
|
210
|
+
let modalClass = 'unknown';
|
|
211
|
+
let modalCount = -1;
|
|
212
|
+
for (const [cls, count] of Object.entries(classCounts)) {
|
|
213
|
+
if (count > modalCount) { modalClass = cls; modalCount = count; }
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
runs: list.length,
|
|
217
|
+
deterministic: Object.keys(classCounts).length === 1,
|
|
218
|
+
classCounts,
|
|
219
|
+
modalClass,
|
|
220
|
+
agreement: modalCount / list.length,
|
|
221
|
+
toolSupportedRatio: toolSupported / list.length,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function redactMetadata(meta) {
|
|
226
|
+
if (!meta || typeof meta !== 'object') return {};
|
|
227
|
+
const out = {};
|
|
228
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
229
|
+
if (SECRET_KEY_RE.test(key)) { out[key] = '[redacted]'; continue; }
|
|
230
|
+
out[key] = typeof value === 'object' && value !== null ? redactMetadata(value) : value;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Opt-in live behavior probe (construct-6zga.1.4 acceptance #2).
|
|
237
|
+
*
|
|
238
|
+
* Requires explicit operator confirmation, bounds the run count, redacts captured
|
|
239
|
+
* metadata, and appends each observation to an append-only sink. It returns
|
|
240
|
+
* evidence and a distribution; it never ranks providers and never writes to the
|
|
241
|
+
* active capability record — promotion is the operator-gated job of the profile
|
|
242
|
+
* inventory (construct-6zga.1.8), not this producer.
|
|
243
|
+
*
|
|
244
|
+
* probeFn is injected so the producer stays provider-agnostic and CI-hermetic;
|
|
245
|
+
* a real run wires it to the W1 probeProviderCapabilities adapter path.
|
|
246
|
+
*/
|
|
247
|
+
export async function runLiveBehaviorProbe(modelId, {
|
|
248
|
+
confirm = false,
|
|
249
|
+
runs = 1,
|
|
250
|
+
transport = 'unknown',
|
|
251
|
+
adapterProtocol = 'unknown',
|
|
252
|
+
probeFn,
|
|
253
|
+
appendSink,
|
|
254
|
+
now = () => new Date().toISOString(),
|
|
255
|
+
} = {}) {
|
|
256
|
+
if (confirm !== true) {
|
|
257
|
+
throw new Error('live behavior probe requires explicit operator confirmation (opts.confirm === true)');
|
|
258
|
+
}
|
|
259
|
+
if (typeof probeFn !== 'function') {
|
|
260
|
+
throw new Error('live behavior probe requires an injected probeFn (provider-agnostic, opt-in)');
|
|
261
|
+
}
|
|
262
|
+
const boundedRuns = Math.max(1, Math.min(MAX_PROBE_RUNS, Number.isInteger(runs) ? runs : 1));
|
|
263
|
+
const observations = [];
|
|
264
|
+
for (let i = 0; i < boundedRuns; i += 1) {
|
|
265
|
+
const raw = await probeFn(modelId, { run: i });
|
|
266
|
+
const measured = raw?.measured || {};
|
|
267
|
+
const observation = buildObservation({
|
|
268
|
+
...raw,
|
|
269
|
+
transport,
|
|
270
|
+
adapterProtocol,
|
|
271
|
+
capabilityClass: classifyCapabilityClass({ transport, measured }),
|
|
272
|
+
evidence: {
|
|
273
|
+
source: 'live_probe',
|
|
274
|
+
capturedAt: now(),
|
|
275
|
+
redacted: true,
|
|
276
|
+
runs: 1,
|
|
277
|
+
distribution: null,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
const record = Object.freeze({ modelId, metadata: Object.freeze(redactMetadata(raw?.metadata)), observation });
|
|
281
|
+
if (typeof appendSink === 'function') await appendSink(record);
|
|
282
|
+
observations.push(observation);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
modelId,
|
|
286
|
+
observations,
|
|
287
|
+
distribution: boundedRuns > 1 ? summarizeProbeRuns(observations) : null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/catalog.mjs — live model catalog merge, cache, and visibility filter.
|
|
3
|
+
*
|
|
4
|
+
* Merges static provider-family hints with cached OpenRouter free-model polls,
|
|
5
|
+
* then applies construct.config.json models.visibility rules so chat, CLI, and
|
|
6
|
+
* dashboard share one filtered catalog surface.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { pollFreeModels } from '../model-free-selector.mjs';
|
|
13
|
+
import { resolveFirstSecret } from '../providers/secret-resolver.mjs';
|
|
14
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
15
|
+
|
|
16
|
+
export const MODEL_VISIBILITY_MODES = ['all_configured', 'tier_defaults', 'explicit'];
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_MODELS_CONFIG = Object.freeze({
|
|
19
|
+
visibility: Object.freeze({
|
|
20
|
+
mode: 'all_configured',
|
|
21
|
+
include: [],
|
|
22
|
+
exclude: [],
|
|
23
|
+
providers: {},
|
|
24
|
+
}),
|
|
25
|
+
catalog: Object.freeze({
|
|
26
|
+
liveOpenRouter: true,
|
|
27
|
+
maxLiveFree: 24,
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const CACHE_FILENAME = 'model-catalog-cache.json';
|
|
32
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
33
|
+
|
|
34
|
+
function uniqueStrings(values = []) {
|
|
35
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.trim()))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cachePath(homeDir = os.homedir()) {
|
|
39
|
+
return path.join(homeDir, '.cx', CACHE_FILENAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveModelsConfig(projectConfig = {}) {
|
|
43
|
+
const models = projectConfig?.models && typeof projectConfig.models === 'object'
|
|
44
|
+
? projectConfig.models
|
|
45
|
+
: {};
|
|
46
|
+
const visibility = { ...DEFAULT_MODELS_CONFIG.visibility, ...(models.visibility || {}) };
|
|
47
|
+
const catalog = { ...DEFAULT_MODELS_CONFIG.catalog, ...(models.catalog || {}) };
|
|
48
|
+
if (!MODEL_VISIBILITY_MODES.includes(visibility.mode)) {
|
|
49
|
+
visibility.mode = DEFAULT_MODELS_CONFIG.visibility.mode;
|
|
50
|
+
}
|
|
51
|
+
visibility.include = Array.isArray(visibility.include) ? visibility.include : [];
|
|
52
|
+
visibility.exclude = Array.isArray(visibility.exclude) ? visibility.exclude : [];
|
|
53
|
+
visibility.providers = visibility.providers && typeof visibility.providers === 'object'
|
|
54
|
+
? visibility.providers
|
|
55
|
+
: {};
|
|
56
|
+
catalog.maxLiveFree = Number.isFinite(catalog.maxLiveFree) ? catalog.maxLiveFree : DEFAULT_MODELS_CONFIG.catalog.maxLiveFree;
|
|
57
|
+
catalog.liveOpenRouter = catalog.liveOpenRouter !== false;
|
|
58
|
+
return { visibility, catalog };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function readLiveCatalogCache({ homeDir = os.homedir(), maxAgeMs = CACHE_TTL_MS } = {}) {
|
|
62
|
+
const file = cachePath(homeDir);
|
|
63
|
+
try {
|
|
64
|
+
if (!fs.existsSync(file)) return null;
|
|
65
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
66
|
+
if (!parsed?.fetchedAt || !Array.isArray(parsed.models)) return null;
|
|
67
|
+
if (Date.now() - parsed.fetchedAt > maxAgeMs) return null;
|
|
68
|
+
return parsed.models;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function writeLiveCatalogCache(models, { homeDir = os.homedir() } = {}) {
|
|
75
|
+
const file = cachePath(homeDir);
|
|
76
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
77
|
+
fs.writeFileSync(file, JSON.stringify({
|
|
78
|
+
fetchedAt: Date.now(),
|
|
79
|
+
models: models.map((m) => ({
|
|
80
|
+
id: m.id,
|
|
81
|
+
name: m.name ?? m.id,
|
|
82
|
+
isFree: m.isFree === true,
|
|
83
|
+
})),
|
|
84
|
+
}, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function refreshLiveOpenRouterCatalog({ env = process.env, homeDir = os.homedir() } = {}) {
|
|
88
|
+
const apiKey = resolveFirstSecret(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env });
|
|
89
|
+
if (!apiKey) return [];
|
|
90
|
+
const models = await pollFreeModels(apiKey);
|
|
91
|
+
writeLiveCatalogCache(models, { homeDir });
|
|
92
|
+
return models;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function providerFamilyEnabled(providerId, visibility) {
|
|
96
|
+
const map = visibility.providers || {};
|
|
97
|
+
if (Object.keys(map).length === 0) return true;
|
|
98
|
+
if (map[providerId] === false) return false;
|
|
99
|
+
if (providerId.startsWith('openrouter') && map.openrouter === false) return false;
|
|
100
|
+
return map[providerId] !== false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function collectTierDefaultIds(registryModels = {}) {
|
|
104
|
+
const ids = [];
|
|
105
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
106
|
+
const def = registryModels[tier];
|
|
107
|
+
if (typeof def === 'string') ids.push(def);
|
|
108
|
+
else if (def && typeof def === 'object') {
|
|
109
|
+
if (def.primary) ids.push(def.primary);
|
|
110
|
+
if (Array.isArray(def.fallback)) ids.push(...def.fallback);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return uniqueStrings(ids);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function mergeLiveModelsIntoProviders(providers, liveModels = [], { maxLiveFree = 24 } = {}) {
|
|
117
|
+
if (!liveModels.length) return providers;
|
|
118
|
+
const liveIds = liveModels.slice(0, maxLiveFree).map((m) => (
|
|
119
|
+
m.id?.startsWith('openrouter/') ? m.id : `openrouter/${m.id}`
|
|
120
|
+
));
|
|
121
|
+
return providers.map((provider) => {
|
|
122
|
+
if (provider.id !== 'openrouter' && !provider.id.startsWith('openrouter')) return provider;
|
|
123
|
+
const options = {
|
|
124
|
+
reasoning: uniqueStrings([...(provider.options?.reasoning ?? []), ...liveIds]),
|
|
125
|
+
standard: uniqueStrings([...(provider.options?.standard ?? []), ...liveIds]),
|
|
126
|
+
fast: uniqueStrings([...(provider.options?.fast ?? []), ...liveIds]),
|
|
127
|
+
};
|
|
128
|
+
return { ...provider, options, liveModelCount: liveIds.length };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function applyModelVisibilityFilter(catalog, {
|
|
133
|
+
visibility = DEFAULT_MODELS_CONFIG.visibility,
|
|
134
|
+
registryModels = {},
|
|
135
|
+
activeModelId = null,
|
|
136
|
+
} = {}) {
|
|
137
|
+
const includeSet = new Set(visibility.include || []);
|
|
138
|
+
const excludeSet = new Set(visibility.exclude || []);
|
|
139
|
+
const tierDefaults = collectTierDefaultIds(registryModels);
|
|
140
|
+
|
|
141
|
+
const modelAllowed = (modelId, providerId) => {
|
|
142
|
+
if (!modelId) return false;
|
|
143
|
+
if (modelId === activeModelId) return true;
|
|
144
|
+
if (excludeSet.has(modelId)) return false;
|
|
145
|
+
if (!providerFamilyEnabled(providerId, visibility)) return false;
|
|
146
|
+
if (visibility.mode === 'explicit') {
|
|
147
|
+
return includeSet.has(modelId);
|
|
148
|
+
}
|
|
149
|
+
if (visibility.mode === 'tier_defaults') {
|
|
150
|
+
return tierDefaults.includes(modelId);
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const providers = catalog.providers
|
|
156
|
+
.filter((provider) => providerFamilyEnabled(provider.id, visibility))
|
|
157
|
+
.map((provider) => {
|
|
158
|
+
const options = {};
|
|
159
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
160
|
+
options[tier] = (provider.options?.[tier] ?? []).filter((id) => modelAllowed(id, provider.id));
|
|
161
|
+
}
|
|
162
|
+
const tiers = { ...provider.tiers };
|
|
163
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
164
|
+
if (tiers[tier] && !modelAllowed(tiers[tier], provider.id)) {
|
|
165
|
+
tiers[tier] = options[tier]?.[0] ?? null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { ...provider, options, tiers };
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const tierOptions = {
|
|
172
|
+
reasoning: uniqueStrings(providers.flatMap((p) => p.options.reasoning)),
|
|
173
|
+
standard: uniqueStrings(providers.flatMap((p) => p.options.standard)),
|
|
174
|
+
fast: uniqueStrings(providers.flatMap((p) => p.options.fast)),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return { providers, tierOptions, visibility, activeModelId };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function loadModelsCatalogContext({ cwd = process.cwd(), env = process.env, homeDir = os.homedir() } = {}) {
|
|
181
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
182
|
+
const modelsConfig = resolveModelsConfig(config);
|
|
183
|
+
let registryModels = {};
|
|
184
|
+
try {
|
|
185
|
+
const registryPath = path.join(cwd, 'specialists', 'registry.json');
|
|
186
|
+
if (fs.existsSync(registryPath)) {
|
|
187
|
+
registryModels = JSON.parse(fs.readFileSync(registryPath, 'utf8')).models ?? {};
|
|
188
|
+
}
|
|
189
|
+
} catch { /* registry read is best-effort outside repo root */ }
|
|
190
|
+
const liveModels = modelsConfig.catalog.liveOpenRouter
|
|
191
|
+
? (readLiveCatalogCache({ homeDir }) ?? [])
|
|
192
|
+
: [];
|
|
193
|
+
return { modelsConfig, registryModels, liveModels };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isModelVisible(modelId, {
|
|
197
|
+
visibility = DEFAULT_MODELS_CONFIG.visibility,
|
|
198
|
+
registryModels = {},
|
|
199
|
+
activeModelId = null,
|
|
200
|
+
providerId = null,
|
|
201
|
+
} = {}) {
|
|
202
|
+
const filtered = applyModelVisibilityFilter(
|
|
203
|
+
{ providers: [{ id: providerId || 'openrouter', options: { reasoning: [modelId], standard: [modelId], fast: [modelId] }, tiers: {} }] },
|
|
204
|
+
{ visibility, registryModels, activeModelId },
|
|
205
|
+
);
|
|
206
|
+
return filtered.providers.some((p) =>
|
|
207
|
+
['reasoning', 'standard', 'fast'].some((tier) => (p.options[tier] ?? []).includes(modelId)),
|
|
208
|
+
);
|
|
209
|
+
}
|