@bubblebrain-ai/bubble 0.0.63 → 0.0.65
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 +437 -11
- package/dist/agent/abort-errors.d.ts +18 -0
- package/dist/agent/abort-errors.js +51 -0
- package/dist/agent/budget-ledger.d.ts +20 -0
- package/dist/agent/budget-ledger.js +45 -0
- package/dist/agent/categories.d.ts +100 -0
- package/dist/agent/categories.js +203 -0
- package/dist/agent/change-tracker.d.ts +40 -0
- package/dist/agent/change-tracker.js +89 -0
- package/dist/agent/child-runner.d.ts +64 -0
- package/dist/agent/child-runner.js +405 -0
- package/dist/agent/input-controller.d.ts +14 -0
- package/dist/agent/input-controller.js +43 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +25 -0
- package/dist/agent/internal-reminder-sanitizer.js +297 -0
- package/dist/agent/profiles.d.ts +76 -0
- package/dist/agent/profiles.js +526 -0
- package/dist/agent/provider-fingerprint.d.ts +6 -0
- package/dist/agent/provider-fingerprint.js +89 -0
- package/dist/agent/resident-history.d.ts +7 -0
- package/dist/agent/resident-history.js +61 -0
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/routing-catalog.d.ts +131 -0
- package/dist/agent/routing-catalog.js +260 -0
- package/dist/agent/structured-output.d.ts +37 -0
- package/dist/agent/structured-output.js +193 -0
- package/dist/agent/subagent/router.d.ts +65 -0
- package/dist/agent/subagent/router.js +203 -0
- package/dist/agent/subagent/runtime.d.ts +235 -0
- package/dist/agent/subagent/runtime.js +835 -0
- package/dist/agent/subagent-control.d.ts +100 -0
- package/dist/agent/subagent-control.js +70 -0
- package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
- package/dist/agent/subagent-lifecycle-reminder.js +112 -0
- package/dist/agent/subagent-route-format.d.ts +8 -0
- package/dist/agent/subagent-route-format.js +18 -0
- package/dist/agent/subagent-scheduler.d.ts +105 -0
- package/dist/agent/subagent-scheduler.js +287 -0
- package/dist/agent/subagent-store.d.ts +57 -0
- package/dist/agent/subagent-store.js +180 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.d.ts +13 -0
- package/dist/agent/subtask-policy.js +56 -0
- package/dist/agent/task-lifecycle-reminder.d.ts +26 -0
- package/dist/agent/task-lifecycle-reminder.js +59 -0
- package/dist/agent/tool-intent.d.ts +45 -0
- package/dist/agent/tool-intent.js +318 -0
- package/dist/agent/tool-visibility.d.ts +10 -0
- package/dist/agent/tool-visibility.js +6 -0
- package/dist/agent/transcript-helpers.d.ts +20 -0
- package/dist/agent/transcript-helpers.js +63 -0
- package/dist/agent/update-queue.d.ts +13 -0
- package/dist/agent/update-queue.js +69 -0
- package/dist/agent/workflow/control.d.ts +60 -0
- package/dist/agent/workflow/control.js +62 -0
- package/dist/agent/workflow/errors.d.ts +16 -0
- package/dist/agent/workflow/errors.js +24 -0
- package/dist/agent/workflow/runs.d.ts +73 -0
- package/dist/agent/workflow/runs.js +160 -0
- package/dist/agent/workflow/runtime.d.ts +88 -0
- package/dist/agent/workflow/runtime.js +319 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +460 -0
- package/dist/agent.js +2210 -0
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +71 -0
- package/dist/approval/controller.js +286 -0
- package/dist/approval/danger.d.ts +13 -0
- package/dist/approval/danger.js +55 -0
- package/dist/approval/diff-hunks.d.ts +12 -0
- package/dist/approval/diff-hunks.js +32 -0
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +21 -0
- package/dist/approval/session-cache.js +38 -0
- package/dist/approval/tool-helper.d.ts +14 -0
- package/dist/approval/tool-helper.js +47 -0
- package/dist/approval/types.d.ts +146 -0
- package/dist/approval/types.js +8 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/bubble-home.d.ts +8 -0
- package/dist/bubble-home.js +19 -0
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.js +175 -0
- package/dist/clipboard.d.ts +14 -0
- package/dist/clipboard.js +132 -0
- package/dist/config.d.ts +96 -0
- package/dist/config.js +287 -0
- package/dist/context/budget.d.ts +28 -0
- package/dist/context/budget.js +130 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.d.ts +25 -0
- package/dist/context/compact-llm.js +175 -0
- package/dist/context/compact.d.ts +124 -0
- package/dist/context/compact.js +635 -0
- package/dist/context/compaction-files.d.ts +44 -0
- package/dist/context/compaction-files.js +148 -0
- package/dist/context/llm-compactor.d.ts +53 -0
- package/dist/context/llm-compactor.js +424 -0
- package/dist/context/overflow.d.ts +9 -0
- package/dist/context/overflow.js +50 -0
- package/dist/context/projector.d.ts +34 -0
- package/dist/context/projector.js +251 -0
- package/dist/context/prune.d.ts +26 -0
- package/dist/context/prune.js +171 -0
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/token-estimator.d.ts +14 -0
- package/dist/context/token-estimator.js +106 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/tool-output-truncate.d.ts +24 -0
- package/dist/context/tool-output-truncate.js +181 -0
- package/dist/context/usage.d.ts +34 -0
- package/dist/context/usage.js +230 -0
- package/dist/debug-trace.d.ts +27 -0
- package/dist/debug-trace.js +401 -0
- package/dist/diff-stats.d.ts +5 -0
- package/dist/diff-stats.js +21 -0
- package/dist/external-runtime/grok-acp.d.ts +56 -0
- package/dist/external-runtime/grok-acp.js +386 -0
- package/dist/external-runtime/grok-binary.d.ts +25 -0
- package/dist/external-runtime/grok-binary.js +108 -0
- package/dist/external-runtime/grok-errors.d.ts +7 -0
- package/dist/external-runtime/grok-errors.js +15 -0
- package/dist/external-runtime/grok-input-policy.d.ts +58 -0
- package/dist/external-runtime/grok-input-policy.js +52 -0
- package/dist/external-runtime/grok-ndjson.d.ts +14 -0
- package/dist/external-runtime/grok-ndjson.js +193 -0
- package/dist/external-runtime/grok-network.d.ts +15 -0
- package/dist/external-runtime/grok-network.js +81 -0
- package/dist/external-runtime/grok-preflight.d.ts +28 -0
- package/dist/external-runtime/grok-preflight.js +194 -0
- package/dist/external-runtime/grok-process.d.ts +26 -0
- package/dist/external-runtime/grok-process.js +184 -0
- package/dist/external-runtime/grok-profile.d.ts +32 -0
- package/dist/external-runtime/grok-profile.js +419 -0
- package/dist/external-runtime/grok-provider.d.ts +31 -0
- package/dist/external-runtime/grok-provider.js +41 -0
- package/dist/external-runtime/grok-runtime.d.ts +91 -0
- package/dist/external-runtime/grok-runtime.js +882 -0
- package/dist/external-runtime/index.d.ts +13 -0
- package/dist/external-runtime/index.js +13 -0
- package/dist/external-runtime/session-policy.d.ts +11 -0
- package/dist/external-runtime/session-policy.js +30 -0
- package/dist/external-runtime/types.d.ts +65 -0
- package/dist/external-runtime/types.js +1 -0
- package/dist/feedback/collect.d.ts +7 -0
- package/dist/feedback/collect.js +119 -0
- package/dist/feedback/config.d.ts +14 -0
- package/dist/feedback/config.js +16 -0
- package/dist/feedback/redact.d.ts +1 -0
- package/dist/feedback/redact.js +25 -0
- package/dist/feedback/submit.d.ts +6 -0
- package/dist/feedback/submit.js +43 -0
- package/dist/feedback/types.d.ts +22 -0
- package/dist/feedback/types.js +1 -0
- package/dist/feishu/agent-host/approval-card.d.ts +11 -0
- package/dist/feishu/agent-host/approval-card.js +85 -0
- package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
- package/dist/feishu/agent-host/approval-ui.js +214 -0
- package/dist/feishu/agent-host/run-driver.d.ts +51 -0
- package/dist/feishu/agent-host/run-driver.js +397 -0
- package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
- package/dist/feishu/agent-host/runtime-deps.js +8 -0
- package/dist/feishu/card/budget.d.ts +40 -0
- package/dist/feishu/card/budget.js +134 -0
- package/dist/feishu/card/renderer.d.ts +29 -0
- package/dist/feishu/card/renderer.js +245 -0
- package/dist/feishu/card/run-state-types.d.ts +49 -0
- package/dist/feishu/card/run-state-types.js +15 -0
- package/dist/feishu/card/run-state.d.ts +21 -0
- package/dist/feishu/card/run-state.js +223 -0
- package/dist/feishu/channel/channel.d.ts +52 -0
- package/dist/feishu/channel/channel.js +74 -0
- package/dist/feishu/config.d.ts +24 -0
- package/dist/feishu/config.js +97 -0
- package/dist/feishu/format.d.ts +6 -0
- package/dist/feishu/format.js +14 -0
- package/dist/feishu/index.d.ts +4 -0
- package/dist/feishu/index.js +4 -0
- package/dist/feishu/logger.d.ts +31 -0
- package/dist/feishu/logger.js +62 -0
- package/dist/feishu/paths.d.ts +12 -0
- package/dist/feishu/paths.js +38 -0
- package/dist/feishu/process-registry.d.ts +29 -0
- package/dist/feishu/process-registry.js +90 -0
- package/dist/feishu/router/commands.d.ts +38 -0
- package/dist/feishu/router/commands.js +286 -0
- package/dist/feishu/router/event-router.d.ts +40 -0
- package/dist/feishu/router/event-router.js +208 -0
- package/dist/feishu/router/whitelist.d.ts +23 -0
- package/dist/feishu/router/whitelist.js +20 -0
- package/dist/feishu/runtime/active-runs.d.ts +32 -0
- package/dist/feishu/runtime/active-runs.js +84 -0
- package/dist/feishu/runtime/pending-queue.d.ts +36 -0
- package/dist/feishu/runtime/pending-queue.js +98 -0
- package/dist/feishu/runtime/process-pool.d.ts +29 -0
- package/dist/feishu/runtime/process-pool.js +49 -0
- package/dist/feishu/schema.d.ts +17 -0
- package/dist/feishu/schema.js +252 -0
- package/dist/feishu/scope/scope-registry.d.ts +39 -0
- package/dist/feishu/scope/scope-registry.js +148 -0
- package/dist/feishu/scope/session-binder.d.ts +44 -0
- package/dist/feishu/scope/session-binder.js +100 -0
- package/dist/feishu/scope/session-store.d.ts +24 -0
- package/dist/feishu/scope/session-store.js +73 -0
- package/dist/feishu/secrets.d.ts +37 -0
- package/dist/feishu/secrets.js +129 -0
- package/dist/feishu/serve.d.ts +12 -0
- package/dist/feishu/serve.js +302 -0
- package/dist/feishu/types.d.ts +75 -0
- package/dist/feishu/types.js +23 -0
- package/dist/feishu/wizard.d.ts +24 -0
- package/dist/feishu/wizard.js +121 -0
- package/dist/goal/command.d.ts +20 -0
- package/dist/goal/command.js +76 -0
- package/dist/goal/engine.d.ts +33 -0
- package/dist/goal/engine.js +65 -0
- package/dist/goal/format.d.ts +18 -0
- package/dist/goal/format.js +112 -0
- package/dist/goal/prompts.d.ts +13 -0
- package/dist/goal/prompts.js +84 -0
- package/dist/goal/store.d.ts +64 -0
- package/dist/goal/store.js +174 -0
- package/dist/goal/tools.d.ts +13 -0
- package/dist/goal/tools.js +67 -0
- package/dist/goal/usage.d.ts +2 -0
- package/dist/goal/usage.js +3 -0
- package/dist/hooks/config.d.ts +9 -0
- package/dist/hooks/config.js +278 -0
- package/dist/hooks/controller.d.ts +24 -0
- package/dist/hooks/controller.js +254 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/log.d.ts +14 -0
- package/dist/hooks/log.js +54 -0
- package/dist/hooks/runner.d.ts +5 -0
- package/dist/hooks/runner.js +225 -0
- package/dist/hooks/trust.d.ts +37 -0
- package/dist/hooks/trust.js +143 -0
- package/dist/hooks/types.d.ts +173 -0
- package/dist/hooks/types.js +46 -0
- package/dist/loop/engine.d.ts +29 -0
- package/dist/loop/engine.js +63 -0
- package/dist/lsp/config.d.ts +18 -0
- package/dist/lsp/config.js +58 -0
- package/dist/lsp/diagnostics.d.ts +24 -0
- package/dist/lsp/diagnostics.js +103 -0
- package/dist/lsp/index.d.ts +3 -0
- package/dist/lsp/index.js +3 -0
- package/dist/lsp/service.d.ts +97 -0
- package/dist/lsp/service.js +815 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +984 -0
- package/dist/mcp/client.d.ts +68 -0
- package/dist/mcp/client.js +177 -0
- package/dist/mcp/config.d.ts +30 -0
- package/dist/mcp/config.js +144 -0
- package/dist/mcp/manager.d.ts +67 -0
- package/dist/mcp/manager.js +328 -0
- package/dist/mcp/name.d.ts +26 -0
- package/dist/mcp/name.js +40 -0
- package/dist/mcp/transports.d.ts +54 -0
- package/dist/mcp/transports.js +267 -0
- package/dist/mcp/types.d.ts +111 -0
- package/dist/mcp/types.js +14 -0
- package/dist/memory/db.d.ts +65 -0
- package/dist/memory/db.js +332 -0
- package/dist/memory/index.d.ts +10 -0
- package/dist/memory/index.js +10 -0
- package/dist/memory/paths.d.ts +18 -0
- package/dist/memory/paths.js +38 -0
- package/dist/memory/phase1.d.ts +23 -0
- package/dist/memory/phase1.js +292 -0
- package/dist/memory/phase2.d.ts +19 -0
- package/dist/memory/phase2.js +119 -0
- package/dist/memory/prompts.d.ts +19 -0
- package/dist/memory/prompts.js +104 -0
- package/dist/memory/reset.d.ts +1 -0
- package/dist/memory/reset.js +13 -0
- package/dist/memory/session-policy.d.ts +25 -0
- package/dist/memory/session-policy.js +55 -0
- package/dist/memory/start.d.ts +24 -0
- package/dist/memory/start.js +50 -0
- package/dist/memory/storage.d.ts +11 -0
- package/dist/memory/storage.js +95 -0
- package/dist/memory/store.d.ts +43 -0
- package/dist/memory/store.js +193 -0
- package/dist/memory/usage.d.ts +1 -0
- package/dist/memory/usage.js +38 -0
- package/dist/model-catalog.d.ts +65 -0
- package/dist/model-catalog.js +399 -0
- package/dist/model-config.d.ts +40 -0
- package/dist/model-config.js +74 -0
- package/dist/model-data.generated.d.ts +16 -0
- package/dist/model-data.generated.js +275 -0
- package/dist/model-pricing.d.ts +54 -0
- package/dist/model-pricing.js +256 -0
- package/dist/model-selection.d.ts +7 -0
- package/dist/model-selection.js +18 -0
- package/dist/network/chatgpt-transport.d.ts +16 -0
- package/dist/network/chatgpt-transport.js +232 -0
- package/dist/network/errors.d.ts +47 -0
- package/dist/network/errors.js +48 -0
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.d.ts +37 -0
- package/dist/network/provider-transport.js +256 -0
- package/dist/network/retry.d.ts +34 -0
- package/dist/network/retry.js +97 -0
- package/dist/network/system-proxy.d.ts +18 -0
- package/dist/network/system-proxy.js +175 -0
- package/dist/oauth/grok.d.ts +26 -0
- package/dist/oauth/grok.js +251 -0
- package/dist/oauth/index.d.ts +4 -0
- package/dist/oauth/index.js +3 -0
- package/dist/oauth/openai-codex.d.ts +15 -0
- package/dist/oauth/openai-codex.js +193 -0
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +88 -0
- package/dist/oauth/storage.js +357 -0
- package/dist/oauth/types.d.ts +15 -0
- package/dist/oauth/types.js +1 -0
- package/dist/orchestrator/default-hooks.d.ts +22 -0
- package/dist/orchestrator/default-hooks.js +127 -0
- package/dist/orchestrator/hooks.d.ts +69 -0
- package/dist/orchestrator/hooks.js +52 -0
- package/dist/permission/mode.d.ts +19 -0
- package/dist/permission/mode.js +16 -0
- package/dist/permissions/rule.d.ts +42 -0
- package/dist/permissions/rule.js +293 -0
- package/dist/permissions/settings.d.ts +97 -0
- package/dist/permissions/settings.js +289 -0
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/permissions/types.d.ts +61 -0
- package/dist/permissions/types.js +14 -0
- package/dist/print-output.d.ts +98 -0
- package/dist/print-output.js +117 -0
- package/dist/prompt/compose.d.ts +19 -0
- package/dist/prompt/compose.js +110 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +100 -0
- package/dist/prompt/environment.d.ts +12 -0
- package/dist/prompt/environment.js +55 -0
- package/dist/prompt/provider-prompts/anthropic.d.ts +1 -0
- package/dist/prompt/provider-prompts/anthropic.js +5 -0
- package/dist/prompt/provider-prompts/codex.d.ts +1 -0
- package/dist/prompt/provider-prompts/codex.js +5 -0
- package/dist/prompt/provider-prompts/deepseek.d.ts +1 -0
- package/dist/prompt/provider-prompts/deepseek.js +7 -0
- package/dist/prompt/provider-prompts/default.d.ts +1 -0
- package/dist/prompt/provider-prompts/default.js +6 -0
- package/dist/prompt/provider-prompts/gemini.d.ts +1 -0
- package/dist/prompt/provider-prompts/gemini.js +5 -0
- package/dist/prompt/provider-prompts/glm.d.ts +1 -0
- package/dist/prompt/provider-prompts/glm.js +7 -0
- package/dist/prompt/provider-prompts/gpt.d.ts +1 -0
- package/dist/prompt/provider-prompts/gpt.js +5 -0
- package/dist/prompt/provider-prompts/kimi.d.ts +1 -0
- package/dist/prompt/provider-prompts/kimi.js +8 -0
- package/dist/prompt/reminders.d.ts +33 -0
- package/dist/prompt/reminders.js +135 -0
- package/dist/prompt/routing.d.ts +14 -0
- package/dist/prompt/routing.js +71 -0
- package/dist/prompt/runtime.d.ts +12 -0
- package/dist/prompt/runtime.js +40 -0
- package/dist/prompt/task-reminders.d.ts +12 -0
- package/dist/prompt/task-reminders.js +64 -0
- package/dist/provider-ai-sdk.d.ts +59 -0
- package/dist/provider-ai-sdk.js +584 -0
- package/dist/provider-anthropic.d.ts +126 -0
- package/dist/provider-anthropic.js +940 -0
- package/dist/provider-ark-responses.d.ts +29 -0
- package/dist/provider-ark-responses.js +518 -0
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider-error-record.d.ts +43 -0
- package/dist/provider-error-record.js +213 -0
- package/dist/provider-grok.d.ts +50 -0
- package/dist/provider-grok.js +110 -0
- package/dist/provider-model-policy.d.ts +16 -0
- package/dist/provider-model-policy.js +33 -0
- package/dist/provider-openai-codex.d.ts +66 -0
- package/dist/provider-openai-codex.js +781 -0
- package/dist/provider-registry.d.ts +197 -0
- package/dist/provider-registry.js +1194 -0
- package/dist/provider-transform.d.ts +12 -0
- package/dist/provider-transform.js +293 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.d.ts +85 -0
- package/dist/provider.js +925 -0
- package/dist/question/controller.d.ts +22 -0
- package/dist/question/controller.js +98 -0
- package/dist/question/index.d.ts +2 -0
- package/dist/question/index.js +2 -0
- package/dist/question/normalize.d.ts +6 -0
- package/dist/question/normalize.js +11 -0
- package/dist/question/types.d.ts +42 -0
- package/dist/question/types.js +6 -0
- package/dist/reasoning-debug.d.ts +7 -0
- package/dist/reasoning-debug.js +33 -0
- package/dist/rewind.d.ts +20 -0
- package/dist/rewind.js +29 -0
- package/dist/sdk/index.d.ts +319 -0
- package/dist/sdk/index.js +1068 -0
- package/dist/sdk/replay-event-log.d.ts +26 -0
- package/dist/sdk/replay-event-log.js +122 -0
- package/dist/sdk/session-turn-coordinator.d.ts +45 -0
- package/dist/sdk/session-turn-coordinator.js +227 -0
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +25 -0
- package/dist/session-log.js +444 -0
- package/dist/session-title.d.ts +16 -0
- package/dist/session-title.js +131 -0
- package/dist/session-types.d.ts +86 -0
- package/dist/session-types.js +1 -0
- package/dist/session.d.ts +135 -0
- package/dist/session.js +664 -0
- package/dist/skills/discovery.d.ts +12 -0
- package/dist/skills/discovery.js +166 -0
- package/dist/skills/format.d.ts +32 -0
- package/dist/skills/format.js +68 -0
- package/dist/skills/frontmatter.d.ts +5 -0
- package/dist/skills/frontmatter.js +60 -0
- package/dist/skills/invocation.d.ts +8 -0
- package/dist/skills/invocation.js +33 -0
- package/dist/skills/registry.d.ts +28 -0
- package/dist/skills/registry.js +79 -0
- package/dist/skills/types.d.ts +34 -0
- package/dist/skills/types.js +1 -0
- package/dist/slash-commands/commands.d.ts +10 -0
- package/dist/slash-commands/commands.js +1251 -0
- package/dist/slash-commands/feishu.d.ts +17 -0
- package/dist/slash-commands/feishu.js +400 -0
- package/dist/slash-commands/index.d.ts +4 -0
- package/dist/slash-commands/index.js +8 -0
- package/dist/slash-commands/registry.d.ts +32 -0
- package/dist/slash-commands/registry.js +73 -0
- package/dist/slash-commands/types.d.ts +109 -0
- package/dist/slash-commands/types.js +1 -0
- package/dist/slash-commands/unified.d.ts +38 -0
- package/dist/slash-commands/unified.js +38 -0
- package/dist/stats/usage.d.ts +59 -0
- package/dist/stats/usage.js +485 -0
- package/dist/stream-json.d.ts +13 -0
- package/dist/stream-json.js +74 -0
- package/dist/system-prompt.d.ts +40 -0
- package/dist/system-prompt.js +7 -0
- package/dist/tasks/manager.d.ts +145 -0
- package/dist/tasks/manager.js +634 -0
- package/dist/tasks/process-utils.d.ts +10 -0
- package/dist/tasks/process-utils.js +49 -0
- package/dist/tasks/promotion.d.ts +17 -0
- package/dist/tasks/promotion.js +30 -0
- package/dist/tasks/wake.d.ts +53 -0
- package/dist/tasks/wake.js +125 -0
- package/dist/text-display.d.ts +3 -0
- package/dist/text-display.js +25 -0
- package/dist/tools/agent-lifecycle.d.ts +30 -0
- package/dist/tools/agent-lifecycle.js +790 -0
- package/dist/tools/bash.d.ts +23 -0
- package/dist/tools/bash.js +443 -0
- package/dist/tools/child-tools.d.ts +41 -0
- package/dist/tools/child-tools.js +122 -0
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +274 -0
- package/dist/tools/edit.d.ts +18 -0
- package/dist/tools/edit.js +200 -0
- package/dist/tools/exa-mcp.d.ts +3 -0
- package/dist/tools/exa-mcp.js +74 -0
- package/dist/tools/exit-plan-mode.d.ts +22 -0
- package/dist/tools/exit-plan-mode.js +79 -0
- package/dist/tools/file-mutation-queue.d.ts +2 -0
- package/dist/tools/file-mutation-queue.js +43 -0
- package/dist/tools/file-state.d.ts +48 -0
- package/dist/tools/file-state.js +76 -0
- package/dist/tools/grep.d.ts +9 -0
- package/dist/tools/grep.js +387 -0
- package/dist/tools/index.d.ts +63 -0
- package/dist/tools/index.js +76 -0
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/lsp.d.ts +4 -0
- package/dist/tools/lsp.js +95 -0
- package/dist/tools/memory.d.ts +8 -0
- package/dist/tools/memory.js +77 -0
- package/dist/tools/path-utils.d.ts +8 -0
- package/dist/tools/path-utils.js +53 -0
- package/dist/tools/prompt-metadata.d.ts +3 -0
- package/dist/tools/prompt-metadata.js +17 -0
- package/dist/tools/question.d.ts +3 -0
- package/dist/tools/question.js +177 -0
- package/dist/tools/read.d.ts +8 -0
- package/dist/tools/read.js +309 -0
- package/dist/tools/sensitive-paths.d.ts +11 -0
- package/dist/tools/sensitive-paths.js +106 -0
- package/dist/tools/server-manager.d.ts +6 -0
- package/dist/tools/server-manager.js +6 -0
- package/dist/tools/server.d.ts +6 -0
- package/dist/tools/server.js +249 -0
- package/dist/tools/skill-search.d.ts +10 -0
- package/dist/tools/skill-search.js +134 -0
- package/dist/tools/skill.d.ts +7 -0
- package/dist/tools/skill.js +81 -0
- package/dist/tools/task-tools.d.ts +10 -0
- package/dist/tools/task-tools.js +133 -0
- package/dist/tools/tool-search.d.ts +23 -0
- package/dist/tools/tool-search.js +145 -0
- package/dist/tools/web-fetch.d.ts +6 -0
- package/dist/tools/web-fetch.js +76 -0
- package/dist/tools/web-search.d.ts +5 -0
- package/dist/tools/web-search.js +50 -0
- package/dist/tools/write.d.ts +10 -0
- package/dist/tools/write.js +155 -0
- package/dist/tui/animation-clock.d.ts +14 -0
- package/dist/tui/animation-clock.js +34 -0
- package/dist/tui/app.d.ts +176 -0
- package/dist/tui/app.js +1659 -0
- package/dist/tui/components/approval-dialog.d.ts +43 -0
- package/dist/tui/components/approval-dialog.js +212 -0
- package/dist/tui/components/bottom-sheet.d.ts +9 -0
- package/dist/tui/components/bottom-sheet.js +32 -0
- package/dist/tui/components/context-info.d.ts +54 -0
- package/dist/tui/components/context-info.js +388 -0
- package/dist/tui/components/feedback-dialog.d.ts +56 -0
- package/dist/tui/components/feedback-dialog.js +262 -0
- package/dist/tui/components/feishu-setup-dialog.d.ts +65 -0
- package/dist/tui/components/feishu-setup-dialog.js +332 -0
- package/dist/tui/components/image-preview.d.ts +32 -0
- package/dist/tui/components/image-preview.js +130 -0
- package/dist/tui/components/plan-dialog.d.ts +34 -0
- package/dist/tui/components/plan-dialog.js +196 -0
- package/dist/tui/components/provider-key-input.d.ts +18 -0
- package/dist/tui/components/provider-key-input.js +49 -0
- package/dist/tui/components/question-dialog.d.ts +25 -0
- package/dist/tui/components/question-dialog.js +263 -0
- package/dist/tui/components/responsive-transcript.d.ts +31 -0
- package/dist/tui/components/responsive-transcript.js +77 -0
- package/dist/tui/components/rewind-picker.d.ts +62 -0
- package/dist/tui/components/rewind-picker.js +317 -0
- package/dist/tui/components/session-picker.d.ts +49 -0
- package/dist/tui/components/session-picker.js +303 -0
- package/dist/tui/components/skills-panel.d.ts +63 -0
- package/dist/tui/components/skills-panel.js +582 -0
- package/dist/tui/components/stats-panel.d.ts +40 -0
- package/dist/tui/components/stats-panel.js +301 -0
- package/dist/tui/components/streaming-message.d.ts +69 -0
- package/dist/tui/components/streaming-message.js +398 -0
- package/dist/tui/components/subagent-inspector.d.ts +28 -0
- package/dist/tui/components/subagent-inspector.js +127 -0
- package/dist/tui/components/task-inspector.d.ts +26 -0
- package/dist/tui/components/task-inspector.js +70 -0
- package/dist/tui/components/tasks-pane.d.ts +126 -0
- package/dist/tui/components/tasks-pane.js +566 -0
- package/dist/tui/components/transcript.d.ts +103 -0
- package/dist/tui/components/transcript.js +858 -0
- package/dist/tui/components/welcome.d.ts +20 -0
- package/dist/tui/components/welcome.js +133 -0
- package/dist/tui/components/workflow-inspector.d.ts +30 -0
- package/dist/tui/components/workflow-inspector.js +136 -0
- package/dist/tui/composer-autocomplete.d.ts +76 -0
- package/dist/tui/composer-autocomplete.js +465 -0
- package/dist/tui/composer-style.d.ts +7 -0
- package/dist/tui/composer-style.js +29 -0
- package/dist/tui/controller/agent-event-reducer.d.ts +75 -0
- package/dist/tui/controller/agent-event-reducer.js +239 -0
- package/dist/tui/controller/composer-controller.d.ts +95 -0
- package/dist/tui/controller/composer-controller.js +502 -0
- package/dist/tui/controller/controller.d.ts +199 -0
- package/dist/tui/controller/controller.js +1034 -0
- package/dist/tui/controller/effects.d.ts +71 -0
- package/dist/tui/controller/effects.js +1 -0
- package/dist/tui/controller/goal-runtime-controller.d.ts +47 -0
- package/dist/tui/controller/goal-runtime-controller.js +202 -0
- package/dist/tui/controller/input-queue-machine.d.ts +71 -0
- package/dist/tui/controller/input-queue-machine.js +121 -0
- package/dist/tui/controller/intents.d.ts +56 -0
- package/dist/tui/controller/intents.js +6 -0
- package/dist/tui/controller/overlay-controller.d.ts +64 -0
- package/dist/tui/controller/overlay-controller.js +129 -0
- package/dist/tui/controller/ports.d.ts +57 -0
- package/dist/tui/controller/ports.js +9 -0
- package/dist/tui/controller/session-transition.d.ts +54 -0
- package/dist/tui/controller/session-transition.js +68 -0
- package/dist/tui/controller/state.d.ts +14 -0
- package/dist/tui/controller/state.js +36 -0
- package/dist/tui/controller/task-runtime-controller.d.ts +66 -0
- package/dist/tui/controller/task-runtime-controller.js +158 -0
- package/dist/tui/detect-theme.d.ts +4 -0
- package/dist/tui/detect-theme.js +116 -0
- package/dist/tui/display-history.d.ts +64 -0
- package/dist/tui/display-history.js +283 -0
- package/dist/tui/display-sanitizer.d.ts +3 -0
- package/dist/tui/display-sanitizer.js +38 -0
- package/dist/tui/edit-diff.d.ts +11 -0
- package/dist/tui/edit-diff.js +57 -0
- package/dist/tui/file-mentions.d.ts +29 -0
- package/dist/tui/file-mentions.js +174 -0
- package/dist/tui/footer.d.ts +37 -0
- package/dist/tui/footer.js +80 -0
- package/dist/tui/formatting/edit-diff.d.ts +11 -0
- package/dist/tui/formatting/edit-diff.js +52 -0
- package/dist/tui/formatting/summary.d.ts +43 -0
- package/dist/tui/formatting/summary.js +150 -0
- package/dist/tui/fullscreen.d.ts +48 -0
- package/dist/tui/fullscreen.js +256 -0
- package/dist/tui/image-display.d.ts +23 -0
- package/dist/tui/image-display.js +92 -0
- package/dist/tui/image-paste.d.ts +113 -0
- package/dist/tui/image-paste.js +565 -0
- package/dist/tui/input-history.d.ts +47 -0
- package/dist/tui/input-history.js +250 -0
- package/dist/tui/markdown-style.d.ts +5 -0
- package/dist/tui/markdown-style.js +23 -0
- package/dist/tui/model/composer-buffer.d.ts +55 -0
- package/dist/tui/model/composer-buffer.js +317 -0
- package/dist/tui/model/composer-types.d.ts +17 -0
- package/dist/tui/model/composer-types.js +1 -0
- package/dist/tui/model/display-history.d.ts +78 -0
- package/dist/tui/model/display-history.js +124 -0
- package/dist/tui/model/display-reconstruct.d.ts +3 -0
- package/dist/tui/model/display-reconstruct.js +130 -0
- package/dist/tui/model/file-mentions.d.ts +31 -0
- package/dist/tui/model/file-mentions.js +178 -0
- package/dist/tui/model/image-attachment.d.ts +15 -0
- package/dist/tui/model/image-attachment.js +1 -0
- package/dist/tui/model/image-paste.d.ts +119 -0
- package/dist/tui/model/image-paste.js +610 -0
- package/dist/tui/model/input-history.d.ts +1 -0
- package/dist/tui/model/input-history.js +1 -0
- package/dist/tui/model/input-queue.d.ts +12 -0
- package/dist/tui/model/input-queue.js +17 -0
- package/dist/tui/model/key-events.d.ts +9 -0
- package/dist/tui/model/key-events.js +8 -0
- package/dist/tui/model/paste-operation-tracker.d.ts +13 -0
- package/dist/tui/model/paste-operation-tracker.js +25 -0
- package/dist/tui/model/recent-activity.d.ts +8 -0
- package/dist/tui/model/recent-activity.js +71 -0
- package/dist/tui/model/subagent-view.d.ts +87 -0
- package/dist/tui/model/subagent-view.js +336 -0
- package/dist/tui/model/submit-dedupe.d.ts +5 -0
- package/dist/tui/model/submit-dedupe.js +25 -0
- package/dist/tui/model/sync-output.d.ts +17 -0
- package/dist/tui/model/sync-output.js +35 -0
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/terminal-env.d.ts +15 -0
- package/dist/tui/model/terminal-env.js +22 -0
- package/dist/tui/model/terminal-mouse.d.ts +28 -0
- package/dist/tui/model/terminal-mouse.js +82 -0
- package/dist/tui/model/theme-style.d.ts +10 -0
- package/dist/tui/model/theme-style.js +79 -0
- package/dist/tui/model/theme.d.ts +72 -0
- package/dist/tui/model/theme.js +121 -0
- package/dist/tui/model/trace-groups.d.ts +45 -0
- package/dist/tui/model/trace-groups.js +558 -0
- package/dist/tui/model/trace-interaction.d.ts +63 -0
- package/dist/tui/model/trace-interaction.js +87 -0
- package/dist/tui/model/width.d.ts +18 -0
- package/dist/tui/model/width.js +130 -0
- package/dist/tui/model-picker-data.d.ts +9 -0
- package/dist/tui/model-picker-data.js +39 -0
- package/dist/tui/model-switch.d.ts +49 -0
- package/dist/tui/model-switch.js +67 -0
- package/dist/tui/paste-placeholder.d.ts +11 -0
- package/dist/tui/paste-placeholder.js +52 -0
- package/dist/tui/recent-activity.d.ts +8 -0
- package/dist/tui/recent-activity.js +71 -0
- package/dist/tui/run-session-picker.d.ts +9 -0
- package/dist/tui/run-session-picker.js +64 -0
- package/dist/tui/run.d.ts +72 -0
- package/dist/tui/run.js +139 -0
- package/dist/tui/session-display.d.ts +6 -0
- package/dist/tui/session-display.js +12 -0
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/terminal/multiplexer.d.ts +5 -0
- package/dist/tui/terminal/multiplexer.js +14 -0
- package/dist/tui/testing/fake-agent.d.ts +24 -0
- package/dist/tui/testing/fake-agent.js +22 -0
- package/dist/tui/testing/spy-host.d.ts +21 -0
- package/dist/tui/testing/spy-host.js +55 -0
- package/dist/tui/trace-groups.d.ts +43 -0
- package/dist/tui/trace-groups.js +554 -0
- package/dist/tui/wordmark.d.ts +18 -0
- package/dist/tui/wordmark.js +208 -0
- package/dist/types.d.ts +503 -0
- package/dist/types.js +4 -0
- package/dist/update/index.d.ts +61 -0
- package/dist/update/index.js +267 -0
- package/dist/variant/thinking-level.d.ts +5 -0
- package/dist/variant/thinking-level.js +26 -0
- package/dist/variant/variant-resolver.d.ts +28 -0
- package/dist/variant/variant-resolver.js +62 -0
- package/node_modules/@bubblebrain-ai/pi-tui/LICENSE +21 -0
- package/node_modules/@bubblebrain-ai/pi-tui/README.md +854 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.d.ts +26 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.js +121 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/alt-screen-search.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.d.ts +86 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.js +649 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/autocomplete.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.d.ts +13 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.js +36 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/alt-screen-flash.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.d.ts +22 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.js +99 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/box.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.d.ts +22 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.js +36 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/cancellable-loader.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.d.ts +330 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.js +2344 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/editor.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.d.ts +7 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.js +43 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/h-stack.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.d.ts +28 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.js +82 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/image.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.d.ts +48 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.js +390 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/input.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.d.ts +31 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.js +67 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/loader.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.d.ts +103 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.js +797 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/markdown.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.d.ts +56 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.js +179 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/scroll-view.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.d.ts +53 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.js +175 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/select-list.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.d.ts +50 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.js +174 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/settings-list.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.d.ts +12 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.js +22 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/spacer.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.d.ts +32 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.js +109 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/stack.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.d.ts +19 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.js +81 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/text.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.d.ts +13 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.js +48 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/truncated-text.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.d.ts +8 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.js +26 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/components/v-stack.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.d.ts +39 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/editor-component.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.d.ts +16 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.js +110 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/fuzzy.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.d.ts +31 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.js +42 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/index.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.d.ts +273 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.js +231 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keybindings.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.d.ts +184 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.js +1174 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/keys.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.d.ts +28 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.js +46 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/kill-ring.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.d.ts +10 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.js +1268 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/latex.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.d.ts +40 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout-node.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.d.ts +42 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.js +335 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/layout.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.d.ts +3 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.js +60 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/native-modifiers.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.d.ts +56 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.js +363 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/stdin-buffer.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.d.ts +10 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.js +59 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-colors.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.d.ts +116 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.js +518 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal-image.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.d.ts +142 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.js +544 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/terminal.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.d.ts +11 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.js +11 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/testing.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +141 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +1340 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.d.ts +39 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.js +536 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-main-screen.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +340 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +1020 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.d.ts +17 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.js +27 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/undo-stack.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.d.ts +96 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.js +1193 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/utils.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.d.ts +25 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.js +96 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/word-navigation.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.d.ts +73 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.d.ts.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.js +188 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/test/virtual-terminal.js.map +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/README.md +20 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/build.sh +62 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/darwin/src/darwin-modifiers.c +70 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/README.md +22 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/build.mjs +229 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/node_modules/@bubblebrain-ai/pi-tui/native/win32/src/win32-console-mode.c +119 -0
- package/node_modules/@bubblebrain-ai/pi-tui/package.json +66 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/package.json +96 -17
- package/bin/bubble.js +0 -39
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-provider registry.
|
|
3
|
+
*
|
|
4
|
+
* Supports OpenAI-compatible providers with dynamic or static model lists.
|
|
5
|
+
* Reads provider configuration from models.json first, then falls back to config.json.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { getBubbleHome } from "./bubble-home.js";
|
|
11
|
+
import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, clearDynamicModelMetadata, getBuiltinModel, getBuiltinProvider, inferGrokModelMetadata, listBuiltinModels, replaceDynamicModelMetadata, } from "./model-catalog.js";
|
|
12
|
+
import { ModelConfig } from "./model-config.js";
|
|
13
|
+
import { AuthStorage } from "./oauth/index.js";
|
|
14
|
+
import { fetchGeminiModels, geminiReasoningLevels } from "./provider-ai-sdk.js";
|
|
15
|
+
import { extractChatGptAccountId, fetchOpenAICodexModelCatalog, getCodexClientVersion } from "./provider-openai-codex.js";
|
|
16
|
+
import { fetchGrokSubscriptionModels } from "./provider-grok.js";
|
|
17
|
+
import { refreshOpenAICodex } from "./oauth/openai-codex.js";
|
|
18
|
+
import { refreshGrok } from "./oauth/grok.js";
|
|
19
|
+
import { THINKING_LEVELS } from "./types.js";
|
|
20
|
+
import { filterProviderModels, OPENROUTER_MODEL_ID, } from "./provider-model-policy.js";
|
|
21
|
+
/** True for providers speaking the OpenAI chat-completions protocol (the default). */
|
|
22
|
+
export function isOpenAICompatibleProtocol(protocol) {
|
|
23
|
+
return protocol === undefined || protocol === "openai-chat";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Ids that are clearly not text chat models. Vendors with one big catalog
|
|
27
|
+
* (alibaba returns 236 entries) would otherwise flood the model picker.
|
|
28
|
+
* Deliberately conservative: vision/preview chat models must pass.
|
|
29
|
+
*/
|
|
30
|
+
const NON_CHAT_ID_PATTERN = /(^|[-_/])(embed|embedding|embeddings|rerank|reranker|tts|asr|whisper|ocr|moderation|guard|image|video|audio|speech|voice|paint|draw)([-_/]|$)/i;
|
|
31
|
+
export function isLikelyChatModelId(id) {
|
|
32
|
+
return !NON_CHAT_ID_PATTERN.test(id);
|
|
33
|
+
}
|
|
34
|
+
/** GET {baseURL}/models, tolerating the common response shapes. */
|
|
35
|
+
async function fetchOpenAICompatibleModelIds(provider) {
|
|
36
|
+
const base = provider.baseURL.replace(/\/+$/, "");
|
|
37
|
+
const response = await fetch(`${base}/models`, {
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${provider.apiKey}`,
|
|
40
|
+
...(provider.headers ?? {}),
|
|
41
|
+
},
|
|
42
|
+
signal: AbortSignal.timeout(MODEL_DISCOVERY_TIMEOUT_MS),
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
throw new Error(`HTTP ${response.status}`);
|
|
46
|
+
const payload = (await response.json());
|
|
47
|
+
const entries = payload?.data ?? payload?.models;
|
|
48
|
+
if (!Array.isArray(entries))
|
|
49
|
+
throw new Error("Model catalog response has no model list.");
|
|
50
|
+
return entries
|
|
51
|
+
.filter((entry) => typeof entry?.id === "string" && entry.id.trim().length > 0)
|
|
52
|
+
.map((entry) => ({
|
|
53
|
+
id: entry.id,
|
|
54
|
+
name: typeof entry.name === "string" ? entry.name : undefined,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
/** Keep only string-valued headers; returns undefined for empty/invalid maps. */
|
|
58
|
+
export function sanitizeProviderHeaders(value) {
|
|
59
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
60
|
+
return undefined;
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
63
|
+
if (typeof raw === "string" && key.trim())
|
|
64
|
+
out[key] = raw;
|
|
65
|
+
}
|
|
66
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
67
|
+
}
|
|
68
|
+
function isCompleteDiscovery(result) {
|
|
69
|
+
return result.source === "remote" && result.authoritative && !result.error;
|
|
70
|
+
}
|
|
71
|
+
const MODEL_DISCOVERY_SUCCESS_TTL_MS = 60_000;
|
|
72
|
+
const MODEL_DISCOVERY_FAILURE_TTL_MS = 10_000;
|
|
73
|
+
/** How long a successful discovery survives on disk before a fresh fetch. */
|
|
74
|
+
const MODEL_DISCOVERY_DISK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
75
|
+
/** Discovery must never delay startup or a model picker for long. */
|
|
76
|
+
const MODEL_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
77
|
+
// Per request: the ChatGPT backend is often reached through a user proxy.
|
|
78
|
+
const OPENAI_CODEX_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
|
|
79
|
+
// Included in the cache identity so a pre-single-model OpenRouter disk cache
|
|
80
|
+
// can never repopulate the picker with the old full remote catalog.
|
|
81
|
+
const OPENROUTER_CATALOG_SCOPE = "ox-alpha-only-v2";
|
|
82
|
+
const OPENROUTER_WIRE_EFFORTS = new Map([
|
|
83
|
+
["none", "off"],
|
|
84
|
+
["minimal", "minimal"],
|
|
85
|
+
["low", "low"],
|
|
86
|
+
["medium", "medium"],
|
|
87
|
+
["high", "high"],
|
|
88
|
+
["xhigh", "xhigh"],
|
|
89
|
+
["max", "max"],
|
|
90
|
+
]);
|
|
91
|
+
function openRouterReasoningMetadata(value, fallback) {
|
|
92
|
+
const fallbackMetadata = {
|
|
93
|
+
reasoningLevels: fallback.reasoningLevels,
|
|
94
|
+
defaultReasoningLevel: fallback.defaultReasoningLevel,
|
|
95
|
+
};
|
|
96
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
97
|
+
return fallbackMetadata;
|
|
98
|
+
const raw = value;
|
|
99
|
+
if (!Array.isArray(raw.supported_efforts))
|
|
100
|
+
return fallbackMetadata;
|
|
101
|
+
const declared = new Set();
|
|
102
|
+
for (const effort of raw.supported_efforts) {
|
|
103
|
+
if (typeof effort !== "string")
|
|
104
|
+
continue;
|
|
105
|
+
const mapped = OPENROUTER_WIRE_EFFORTS.get(effort);
|
|
106
|
+
if (mapped)
|
|
107
|
+
declared.add(mapped);
|
|
108
|
+
}
|
|
109
|
+
if (raw.mandatory === true)
|
|
110
|
+
declared.delete("off");
|
|
111
|
+
const reasoningLevels = THINKING_LEVELS.filter((level) => declared.has(level));
|
|
112
|
+
if (reasoningLevels.length === 0)
|
|
113
|
+
return fallbackMetadata;
|
|
114
|
+
const rawDefault = typeof raw.default_effort === "string"
|
|
115
|
+
? OPENROUTER_WIRE_EFFORTS.get(raw.default_effort)
|
|
116
|
+
: undefined;
|
|
117
|
+
const defaultReasoningLevel = rawDefault && reasoningLevels.includes(rawDefault)
|
|
118
|
+
? rawDefault
|
|
119
|
+
: fallback.defaultReasoningLevel && reasoningLevels.includes(fallback.defaultReasoningLevel)
|
|
120
|
+
? fallback.defaultReasoningLevel
|
|
121
|
+
: reasoningLevels.includes("medium")
|
|
122
|
+
? "medium"
|
|
123
|
+
: reasoningLevels[0];
|
|
124
|
+
return { reasoningLevels, defaultReasoningLevel };
|
|
125
|
+
}
|
|
126
|
+
export const BUILTIN_PROVIDERS = CATALOG_PROVIDERS;
|
|
127
|
+
export const USER_VISIBLE_PROVIDER_IDS = BUILTIN_PROVIDERS
|
|
128
|
+
.filter((provider) => !provider.hidden && provider.id !== "openai-codex")
|
|
129
|
+
.map((provider) => provider.id);
|
|
130
|
+
export function isUserVisibleProvider(providerId) {
|
|
131
|
+
return USER_VISIBLE_PROVIDER_IDS.includes(providerId);
|
|
132
|
+
}
|
|
133
|
+
/** Judge expiry on the exact credential set the caller is about to send. */
|
|
134
|
+
function credentialsExpired(credentials, graceMs = 5 * 60 * 1000) {
|
|
135
|
+
return Date.now() >= credentials.expiresAt - graceMs;
|
|
136
|
+
}
|
|
137
|
+
export class ProviderRegistry {
|
|
138
|
+
config;
|
|
139
|
+
modelConfig;
|
|
140
|
+
authStorage;
|
|
141
|
+
modelDiscoveryCache = new Map();
|
|
142
|
+
discoveryDiskCachePath = join(getBubbleHome(), "model-discovery-cache.json");
|
|
143
|
+
modelDiscoveryInFlight = new Map();
|
|
144
|
+
modelDiscoveryGeneration = new Map();
|
|
145
|
+
/** Last membership seen per discovery key — survives cache eviction/TTL so
|
|
146
|
+
* a re-discovery with identical membership never bumps the revision. */
|
|
147
|
+
lastDiscoveryMembership = new Map();
|
|
148
|
+
routingRevision = 0;
|
|
149
|
+
identityFingerprints = new Map();
|
|
150
|
+
constructor(config) {
|
|
151
|
+
this.config = config;
|
|
152
|
+
this.modelConfig = new ModelConfig();
|
|
153
|
+
this.authStorage = new AuthStorage();
|
|
154
|
+
for (const authKey of this.authStorage.list()) {
|
|
155
|
+
const fingerprint = this.credentialFingerprint(authKey);
|
|
156
|
+
if (fingerprint)
|
|
157
|
+
this.identityFingerprints.set(authKey, fingerprint);
|
|
158
|
+
}
|
|
159
|
+
this.authStorage.onMutation((authKey) => this.handleAuthMutation(authKey));
|
|
160
|
+
this.loadDiscoveryDiskCache();
|
|
161
|
+
}
|
|
162
|
+
/** The on-disk cache is production-only; disable it under the test runner so
|
|
163
|
+
* tests sharing one isolated BUBBLE_HOME never contaminate each other. */
|
|
164
|
+
get discoveryDiskCacheEnabled() {
|
|
165
|
+
const v = process.env.VITEST?.trim().toLowerCase();
|
|
166
|
+
return v !== "true" && v !== "1";
|
|
167
|
+
}
|
|
168
|
+
/** Restore a prior successful discovery so dynamic models resolve without a
|
|
169
|
+
* network round-trip right after startup (no fetch on every launch). */
|
|
170
|
+
loadDiscoveryDiskCache() {
|
|
171
|
+
if (!this.discoveryDiskCacheEnabled)
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
if (!existsSync(this.discoveryDiskCachePath))
|
|
175
|
+
return;
|
|
176
|
+
const parsed = JSON.parse(readFileSync(this.discoveryDiskCachePath, "utf8"));
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
for (const [key, entry] of Object.entries(parsed)) {
|
|
179
|
+
if (!entry || typeof entry.expiresAt !== "number")
|
|
180
|
+
continue;
|
|
181
|
+
if (!entry.result || !Array.isArray(entry.result.models))
|
|
182
|
+
continue;
|
|
183
|
+
if (entry.expiresAt <= now && !(entry.providerId === "openai" && isCompleteDiscovery(entry.result)))
|
|
184
|
+
continue;
|
|
185
|
+
const provider = { id: entry.providerId ?? "", authType: entry.authType, protocol: entry.protocol };
|
|
186
|
+
// Capability fields may have been derived by an older Bubble build.
|
|
187
|
+
// Recompute them with the current code while retaining cached remote
|
|
188
|
+
// membership, display names and context windows.
|
|
189
|
+
const result = normalizeProviderDiscoveryMetadata(provider, entry.result);
|
|
190
|
+
this.modelDiscoveryCache.set(key, {
|
|
191
|
+
result,
|
|
192
|
+
expiresAt: entry.expiresAt,
|
|
193
|
+
identityKey: entry.identityKey ?? "unknown",
|
|
194
|
+
providerId: provider.id,
|
|
195
|
+
authType: entry.authType,
|
|
196
|
+
protocol: entry.protocol,
|
|
197
|
+
confirmed: isCompleteDiscovery(result) ? { models: result.models, until: entry.expiresAt } : undefined,
|
|
198
|
+
});
|
|
199
|
+
// Rebuild the dynamic overlay so context window / reasoning levels
|
|
200
|
+
// resolve from the cached catalog at startup, not only on /model open.
|
|
201
|
+
if (entry.providerId) {
|
|
202
|
+
this.applyDynamicDiscoveryMetadata(provider, result);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Missing/corrupt cache is fine; the next discovery re-fetches.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Persist successful discoveries so the next launch can reuse them. */
|
|
211
|
+
saveDiscoveryDiskCache() {
|
|
212
|
+
if (!this.discoveryDiskCacheEnabled)
|
|
213
|
+
return;
|
|
214
|
+
try {
|
|
215
|
+
const data = {};
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
for (const [key, entry] of this.modelDiscoveryCache) {
|
|
218
|
+
const common = {
|
|
219
|
+
identityKey: entry.identityKey,
|
|
220
|
+
providerId: entry.providerId,
|
|
221
|
+
authType: entry.authType,
|
|
222
|
+
protocol: entry.protocol,
|
|
223
|
+
};
|
|
224
|
+
if (!entry.result.error) {
|
|
225
|
+
data[key] = { ...common, result: entry.result, expiresAt: now + MODEL_DISCOVERY_DISK_TTL_MS };
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// A failed refresh must not erase the identity's last confirmed catalog
|
|
229
|
+
// from disk when an unrelated provider triggers a rewrite; persist the
|
|
230
|
+
// confirmation under its remaining horizon instead of dropping the key.
|
|
231
|
+
if (entry.confirmed && (entry.confirmed.until > now || entry.providerId === "openai")) {
|
|
232
|
+
data[key] = {
|
|
233
|
+
...common,
|
|
234
|
+
result: { models: entry.confirmed.models, source: "remote", authoritative: true },
|
|
235
|
+
expiresAt: entry.confirmed.until,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
mkdirSync(dirname(this.discoveryDiskCachePath), { recursive: true });
|
|
240
|
+
writeFileSync(this.discoveryDiskCachePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// Persistence is best-effort.
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Monotonic counter of semantic routing-world changes: provider set,
|
|
248
|
+
* credential identity, provider key/config, discovery membership.
|
|
249
|
+
* Same-account OAuth token rotation deliberately does NOT bump it
|
|
250
|
+
* (docs/model-routing-design.md §1.6).
|
|
251
|
+
*/
|
|
252
|
+
getRoutingRevision() {
|
|
253
|
+
// A login/logout in another Bubble process only reaches handleAuthMutation
|
|
254
|
+
// when the storage notices the file changed; callers cache against this
|
|
255
|
+
// number without touching credentials, so look now (one stat).
|
|
256
|
+
this.authStorage.sync();
|
|
257
|
+
return this.routingRevision;
|
|
258
|
+
}
|
|
259
|
+
bumpRoutingRevision() {
|
|
260
|
+
this.routingRevision++;
|
|
261
|
+
}
|
|
262
|
+
/** Stable identity for stored credentials: account id, else refresh-token hash. */
|
|
263
|
+
credentialFingerprint(authKey) {
|
|
264
|
+
const credentials = this.authStorage.get(authKey);
|
|
265
|
+
if (!credentials)
|
|
266
|
+
return undefined;
|
|
267
|
+
if (credentials.accountId)
|
|
268
|
+
return credentials.accountId;
|
|
269
|
+
const stable = credentials.refreshToken || credentials.accessToken;
|
|
270
|
+
return createHash("sha256").update(stable).digest("hex").slice(0, 16);
|
|
271
|
+
}
|
|
272
|
+
handleAuthMutation(authKey) {
|
|
273
|
+
const next = this.credentialFingerprint(authKey);
|
|
274
|
+
const prev = this.identityFingerprints.get(authKey);
|
|
275
|
+
if (next === prev)
|
|
276
|
+
return; // same-identity token rotation: not a routing change
|
|
277
|
+
if (next === undefined) {
|
|
278
|
+
this.identityFingerprints.delete(authKey);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
this.identityFingerprints.set(authKey, next);
|
|
282
|
+
}
|
|
283
|
+
this.bumpRoutingRevision();
|
|
284
|
+
}
|
|
285
|
+
getModelConfig() {
|
|
286
|
+
return this.modelConfig;
|
|
287
|
+
}
|
|
288
|
+
getAuthStorage() {
|
|
289
|
+
return this.authStorage;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Run discovery for an account-scoped (OAuth) provider so the routing
|
|
293
|
+
* snapshot knows the account's real catalog before the routing prompt is
|
|
294
|
+
* composed and before the first subagent spawn. Discovery was previously
|
|
295
|
+
* triggered only by the model picker, which left tier routing on the static
|
|
296
|
+
* builtin list for the whole session unless the user opened /model.
|
|
297
|
+
* Resolves when discovery settles (it never throws: failures resolve to the
|
|
298
|
+
* local fallback); resolves immediately when a fresh cache exists or the
|
|
299
|
+
* provider is not account-scoped. Hosts bound the wait, see waitForModelDiscovery.
|
|
300
|
+
*/
|
|
301
|
+
warmModelDiscovery(providerId) {
|
|
302
|
+
const provider = this.getConfigured().find((item) => item.id === providerId);
|
|
303
|
+
if (!provider?.enabled || !provider.apiKey || provider.authType !== "oauth")
|
|
304
|
+
return Promise.resolve();
|
|
305
|
+
if (this.getCachedDiscoverySnapshot(providerId))
|
|
306
|
+
return Promise.resolve();
|
|
307
|
+
return this.discoverModels(provider).then(() => undefined, () => undefined);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Warm discovery but never hold startup hostage to the network: after
|
|
311
|
+
* `maxWaitMs` the host proceeds with whatever the snapshot has, and the
|
|
312
|
+
* still-running discovery updates the live accessor when it lands.
|
|
313
|
+
*/
|
|
314
|
+
async waitForModelDiscovery(providerId, maxWaitMs) {
|
|
315
|
+
let timer;
|
|
316
|
+
const deadline = new Promise((resolve) => {
|
|
317
|
+
timer = setTimeout(resolve, maxWaitMs);
|
|
318
|
+
timer.unref?.();
|
|
319
|
+
});
|
|
320
|
+
try {
|
|
321
|
+
await Promise.race([this.warmModelDiscovery(providerId), deadline]);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
if (timer)
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
supportsOAuth(providerId) {
|
|
329
|
+
return !!getBuiltinProvider(providerId)?.supportsOAuth;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Auth-storage keys currently backing a provider's OAuth login. The visible
|
|
333
|
+
* `openai` provider may still be backed by the legacy `openai-codex` key,
|
|
334
|
+
* so UI state and logout must not check `has(providerId)` directly.
|
|
335
|
+
*/
|
|
336
|
+
getOAuthLoginKeys(providerId) {
|
|
337
|
+
const candidates = providerId === "openai" || providerId === "openai-codex"
|
|
338
|
+
? ["openai", "openai-codex"]
|
|
339
|
+
: [providerId];
|
|
340
|
+
return candidates.filter((key) => this.authStorage.has(key));
|
|
341
|
+
}
|
|
342
|
+
resolveOAuthAuthKey(providerId) {
|
|
343
|
+
if (providerId === "openai" || providerId === "openai-codex") {
|
|
344
|
+
if (this.authStorage.has("openai"))
|
|
345
|
+
return "openai";
|
|
346
|
+
if (this.authStorage.has("openai-codex"))
|
|
347
|
+
return "openai-codex";
|
|
348
|
+
}
|
|
349
|
+
return providerId;
|
|
350
|
+
}
|
|
351
|
+
createOpenAICodexAuthAdapter(providerId) {
|
|
352
|
+
if (providerId !== "openai" && providerId !== "openai-codex")
|
|
353
|
+
return undefined;
|
|
354
|
+
if (!this.authStorage.has(this.resolveOAuthAuthKey(providerId)))
|
|
355
|
+
return undefined;
|
|
356
|
+
const readCredentials = () => this.authStorage.get(this.resolveOAuthAuthKey(providerId));
|
|
357
|
+
let refreshPromise;
|
|
358
|
+
return {
|
|
359
|
+
getCredentials: readCredentials,
|
|
360
|
+
isExpired: credentialsExpired,
|
|
361
|
+
refreshCredentials: async (used) => {
|
|
362
|
+
if (!refreshPromise) {
|
|
363
|
+
refreshPromise = this.refreshOpenAICredentials(providerId, used).finally(() => {
|
|
364
|
+
refreshPromise = undefined;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return refreshPromise;
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
createGrokAuthAdapter(providerId) {
|
|
372
|
+
if (providerId !== "grok" || !this.authStorage.has("grok"))
|
|
373
|
+
return undefined;
|
|
374
|
+
let refreshPromise;
|
|
375
|
+
return {
|
|
376
|
+
getCredentials: () => this.authStorage.get("grok"),
|
|
377
|
+
isExpired: credentialsExpired,
|
|
378
|
+
refreshCredentials: async (used) => {
|
|
379
|
+
if (!refreshPromise) {
|
|
380
|
+
refreshPromise = this.refreshGrokCredentials(used).finally(() => {
|
|
381
|
+
refreshPromise = undefined;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return refreshPromise;
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
getDefaultModel(providerId, authType = "api") {
|
|
389
|
+
const customModels = filterProviderModels(providerId, this.modelConfig.getCustomModels(providerId));
|
|
390
|
+
if (customModels.length > 0) {
|
|
391
|
+
return customModels[0].id;
|
|
392
|
+
}
|
|
393
|
+
if (providerId === "openai" && authType === "oauth") {
|
|
394
|
+
return listBuiltinModels("openai-codex")[0]?.id;
|
|
395
|
+
}
|
|
396
|
+
return listBuiltinModels(providerId)[0]?.id;
|
|
397
|
+
}
|
|
398
|
+
async prepareProvider(providerId) {
|
|
399
|
+
// One snapshot decides both "is it expired" and "what did we hold": with
|
|
400
|
+
// two reads another process could rotate in between, and the fresh set
|
|
401
|
+
// would be taken for the held one and refreshed again for nothing.
|
|
402
|
+
if (providerId === "grok") {
|
|
403
|
+
const held = this.authStorage.get("grok");
|
|
404
|
+
if (held?.refreshToken && credentialsExpired(held))
|
|
405
|
+
await this.refreshGrokCredentials(held);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (providerId !== "openai" && providerId !== "openai-codex")
|
|
409
|
+
return;
|
|
410
|
+
const held = this.authStorage.get(this.resolveOAuthAuthKey(providerId));
|
|
411
|
+
if (held?.refreshToken && credentialsExpired(held))
|
|
412
|
+
await this.refreshOpenAICredentials(providerId, held);
|
|
413
|
+
}
|
|
414
|
+
refreshOpenAICredentials(providerId, used) {
|
|
415
|
+
return this.refreshSharedCredentials({
|
|
416
|
+
// Resolved under the lock: another process may have completed a fresh
|
|
417
|
+
// /login openai meanwhile, and a legacy openai-codex refresh must not be
|
|
418
|
+
// mirrored over it.
|
|
419
|
+
resolveKeys: () => {
|
|
420
|
+
const authKey = this.resolveOAuthAuthKey(providerId);
|
|
421
|
+
// "openai" is the canonical key; a legacy openai-codex login is mirrored.
|
|
422
|
+
return { authKey, writeKeys: authKey === "openai" ? ["openai"] : ["openai", authKey] };
|
|
423
|
+
},
|
|
424
|
+
used,
|
|
425
|
+
label: "OpenAI",
|
|
426
|
+
loginHint: "/login openai",
|
|
427
|
+
refresh: async (current) => {
|
|
428
|
+
const refreshed = await refreshOpenAICodex(current.refreshToken);
|
|
429
|
+
return {
|
|
430
|
+
type: "oauth",
|
|
431
|
+
accessToken: refreshed.accessToken,
|
|
432
|
+
refreshToken: refreshed.refreshToken,
|
|
433
|
+
expiresAt: refreshed.expiresAt,
|
|
434
|
+
idToken: refreshed.idToken || current.idToken,
|
|
435
|
+
accountId: refreshed.accountId || current.accountId,
|
|
436
|
+
};
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
refreshGrokCredentials(used) {
|
|
441
|
+
return this.refreshSharedCredentials({
|
|
442
|
+
resolveKeys: () => ({ authKey: "grok", writeKeys: ["grok"] }),
|
|
443
|
+
used,
|
|
444
|
+
label: "Grok",
|
|
445
|
+
loginHint: "/login grok",
|
|
446
|
+
refresh: async (current) => {
|
|
447
|
+
const refreshed = await refreshGrok(current.refreshToken);
|
|
448
|
+
return {
|
|
449
|
+
type: "oauth",
|
|
450
|
+
accessToken: refreshed.accessToken,
|
|
451
|
+
refreshToken: refreshed.refreshToken,
|
|
452
|
+
expiresAt: refreshed.expiresAt,
|
|
453
|
+
};
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* The one place a refresh token is spent. auth.json is shared by every
|
|
459
|
+
* Bubble process and refresh tokens are single-use, so the token is only
|
|
460
|
+
* presented under the cross-process lock and only after re-reading the file:
|
|
461
|
+
* if another process already rotated it, its result is adopted and no
|
|
462
|
+
* request is made. A rejected token gets the same second look before the
|
|
463
|
+
* error surfaces, because the rotation may have landed mid-request.
|
|
464
|
+
*
|
|
465
|
+
* `used` is the credential set the caller was holding when it asked (the
|
|
466
|
+
* provider passes it, also for a forced refresh after a 401): "rotated" means
|
|
467
|
+
* the file no longer holds that set, not merely that it is unexpired.
|
|
468
|
+
*/
|
|
469
|
+
refreshSharedCredentials(options) {
|
|
470
|
+
const { label, loginHint, refresh } = options;
|
|
471
|
+
const held = options.used ?? this.authStorage.get(options.resolveKeys().authKey);
|
|
472
|
+
return this.authStorage.withRefreshLock(async () => {
|
|
473
|
+
this.authStorage.reload();
|
|
474
|
+
const { authKey, writeKeys } = options.resolveKeys();
|
|
475
|
+
const current = this.authStorage.get(authKey);
|
|
476
|
+
if (!current?.refreshToken) {
|
|
477
|
+
throw new Error(`${label} OAuth credentials are missing a refresh token. Run ${loginHint} again.`);
|
|
478
|
+
}
|
|
479
|
+
const rotatedElsewhere = (since) => {
|
|
480
|
+
this.authStorage.reload();
|
|
481
|
+
// Re-resolved every time: a login elsewhere may have created the
|
|
482
|
+
// canonical key since, and it is then the entry to adopt.
|
|
483
|
+
const latest = this.authStorage.get(options.resolveKeys().authKey);
|
|
484
|
+
if (!latest || !since)
|
|
485
|
+
return undefined;
|
|
486
|
+
if (latest.refreshToken === since.refreshToken && latest.accessToken === since.accessToken)
|
|
487
|
+
return undefined;
|
|
488
|
+
return credentialsExpired(latest) ? undefined : latest;
|
|
489
|
+
};
|
|
490
|
+
const adopted = rotatedElsewhere(held);
|
|
491
|
+
if (adopted)
|
|
492
|
+
return adopted;
|
|
493
|
+
let next;
|
|
494
|
+
try {
|
|
495
|
+
next = await refresh(current);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
const rescued = rotatedElsewhere(current);
|
|
499
|
+
if (rescued)
|
|
500
|
+
return rescued;
|
|
501
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
502
|
+
// A 400/401 from the token endpoint is a verdict on the refresh token
|
|
503
|
+
// (reused, revoked, expired), not a transient failure: say what to do.
|
|
504
|
+
if (/Token refresh failed: 40[01]\b|refresh_token_reused|invalid_grant/.test(message)) {
|
|
505
|
+
throw new Error(`${label} login is no longer valid. Run ${loginHint} to sign in again.\n${message}`);
|
|
506
|
+
}
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
// A /login or /logout elsewhere may have landed while the request was in
|
|
510
|
+
// flight (they do not take the refresh lock): their outcome wins.
|
|
511
|
+
if (!this.authStorage.replaceIfUnchanged(authKey, next, current.refreshToken)) {
|
|
512
|
+
const replaced = this.authStorage.get(authKey);
|
|
513
|
+
if (replaced)
|
|
514
|
+
return replaced;
|
|
515
|
+
throw new Error(`${label} credentials were removed while refreshing. Run ${loginHint} to sign in again.`);
|
|
516
|
+
}
|
|
517
|
+
for (const key of writeKeys) {
|
|
518
|
+
if (key === authKey)
|
|
519
|
+
continue;
|
|
520
|
+
// Mirror keys were absent when resolved under the lock. If a login
|
|
521
|
+
// created one since, it is now the preferred entry: leave it alone and
|
|
522
|
+
// hand its credentials to the caller instead of the old account's.
|
|
523
|
+
if (!this.authStorage.replaceIfUnchanged(key, next, null)) {
|
|
524
|
+
const preferred = this.authStorage.get(options.resolveKeys().authKey);
|
|
525
|
+
if (preferred && !credentialsExpired(preferred))
|
|
526
|
+
return preferred;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return next;
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
getConfigured() {
|
|
533
|
+
// 1. Try models.json first
|
|
534
|
+
const modelsJsonProviders = this.modelConfig.getAllProviders();
|
|
535
|
+
const keys = Object.keys(modelsJsonProviders);
|
|
536
|
+
let providers = [];
|
|
537
|
+
if (keys.length > 0) {
|
|
538
|
+
providers = keys.map((id) => {
|
|
539
|
+
const builtin = getBuiltinProvider(id);
|
|
540
|
+
const cfg = modelsJsonProviders[id];
|
|
541
|
+
const baseURL = upgradeLegacyBaseURL(id, cfg.baseURL || builtin?.baseURL || "", cfg.protocol);
|
|
542
|
+
return {
|
|
543
|
+
id,
|
|
544
|
+
name: builtin?.name || id,
|
|
545
|
+
baseURL,
|
|
546
|
+
apiKey: cfg.apiKey || "",
|
|
547
|
+
enabled: true,
|
|
548
|
+
authType: "api",
|
|
549
|
+
protocol: resolveConfiguredProtocol(id, baseURL, cfg.protocol),
|
|
550
|
+
headers: sanitizeProviderHeaders(cfg.headers),
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
// 2. Fall back to config.json providers (interactive TUI style)
|
|
556
|
+
providers = this.config.getProviders().map((provider) => {
|
|
557
|
+
// config.json entries may omit baseURL (a bare {id, apiKey} written
|
|
558
|
+
// by minimal setups / benchmark harnesses); merge the builtin default
|
|
559
|
+
// the same way the models.json path above does, instead of propagating
|
|
560
|
+
// undefined into provider construction.
|
|
561
|
+
const builtinBase = getBuiltinProvider(provider.id)?.baseURL ?? "";
|
|
562
|
+
const rawBaseURL = provider.baseURL || builtinBase;
|
|
563
|
+
const baseURL = upgradeLegacyBaseURL(provider.id, rawBaseURL, provider.protocol);
|
|
564
|
+
return {
|
|
565
|
+
...provider,
|
|
566
|
+
baseURL,
|
|
567
|
+
protocol: resolveConfiguredProtocol(provider.id, baseURL, provider.protocol),
|
|
568
|
+
headers: sanitizeProviderHeaders(provider.headers),
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
// 3. Inject OAuth access tokens
|
|
573
|
+
for (const p of providers) {
|
|
574
|
+
const authKey = this.resolveOAuthAuthKey(p.id);
|
|
575
|
+
if (this.authStorage.has(authKey)) {
|
|
576
|
+
const token = this.authStorage.getAccessToken(authKey);
|
|
577
|
+
if (token) {
|
|
578
|
+
p.apiKey = token;
|
|
579
|
+
p.authType = "oauth";
|
|
580
|
+
if (p.id === "openai") {
|
|
581
|
+
p.baseURL = "https://chatgpt.com/backend-api";
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// 4. Auto-include built-in OAuth providers that have credentials
|
|
587
|
+
const configuredIds = new Set(providers.map((p) => p.id));
|
|
588
|
+
for (const builtin of BUILTIN_PROVIDERS) {
|
|
589
|
+
if (builtin.id === "openai-codex")
|
|
590
|
+
continue;
|
|
591
|
+
if (configuredIds.has(builtin.id))
|
|
592
|
+
continue;
|
|
593
|
+
const authKey = this.resolveOAuthAuthKey(builtin.id);
|
|
594
|
+
if (this.authStorage.has(authKey)) {
|
|
595
|
+
const token = this.authStorage.getAccessToken(authKey);
|
|
596
|
+
providers.push({
|
|
597
|
+
...builtin,
|
|
598
|
+
apiKey: token || "",
|
|
599
|
+
enabled: !!token,
|
|
600
|
+
authType: "oauth",
|
|
601
|
+
...(builtin.id === "openai" ? { baseURL: "https://chatgpt.com/backend-api" } : {}),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// OAuth providers can be injected even when models.json owns the model
|
|
606
|
+
// catalog. Honor the separately stored enable preference in either path.
|
|
607
|
+
const enablePreferences = new Map(this.config.getProviders().map((p) => [p.id, p.enabled]));
|
|
608
|
+
for (const provider of providers) {
|
|
609
|
+
if (provider.authType === "oauth" && enablePreferences.has(provider.id)) {
|
|
610
|
+
provider.enabled = enablePreferences.get(provider.id) !== false && !!provider.apiKey;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return providers;
|
|
614
|
+
}
|
|
615
|
+
getEnabled() {
|
|
616
|
+
return this.getConfigured().filter((p) => p.enabled && p.apiKey);
|
|
617
|
+
}
|
|
618
|
+
getDefault() {
|
|
619
|
+
const enabled = this.getEnabled();
|
|
620
|
+
if (enabled.length === 0)
|
|
621
|
+
return undefined;
|
|
622
|
+
const defaultId = this.config.getDefaultProvider();
|
|
623
|
+
const preferred = enabled.filter((provider) => isUserVisibleProvider(provider.id));
|
|
624
|
+
return preferred.find((p) => p.id === defaultId)
|
|
625
|
+
|| preferred[0]
|
|
626
|
+
|| enabled.find((p) => p.id === defaultId)
|
|
627
|
+
|| enabled[0];
|
|
628
|
+
}
|
|
629
|
+
setDefault(id) {
|
|
630
|
+
this.config.setDefaultProvider(id);
|
|
631
|
+
}
|
|
632
|
+
addProvider(id, apiKey) {
|
|
633
|
+
const builtin = getBuiltinProvider(id);
|
|
634
|
+
if (!builtin)
|
|
635
|
+
return false;
|
|
636
|
+
const providers = this.config.getProviders();
|
|
637
|
+
const idx = providers.findIndex((p) => p.id === id);
|
|
638
|
+
const profile = { ...builtin, apiKey, enabled: true };
|
|
639
|
+
if (idx >= 0) {
|
|
640
|
+
providers[idx] = profile;
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
providers.push(profile);
|
|
644
|
+
}
|
|
645
|
+
this.config.setProviders(providers);
|
|
646
|
+
this.bumpRoutingRevision();
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
removeProvider(id) {
|
|
650
|
+
const providers = this.config.getProviders().filter((p) => p.id !== id);
|
|
651
|
+
this.config.setProviders(providers);
|
|
652
|
+
this.bumpRoutingRevision();
|
|
653
|
+
}
|
|
654
|
+
updateProviderKey(id, apiKey) {
|
|
655
|
+
const providers = this.config.getProviders();
|
|
656
|
+
const p = providers.find((x) => x.id === id);
|
|
657
|
+
if (p) {
|
|
658
|
+
p.apiKey = apiKey;
|
|
659
|
+
this.config.setProviders(providers);
|
|
660
|
+
this.bumpRoutingRevision();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async listModels(provider) {
|
|
664
|
+
return (await this.discoverModels(provider)).models;
|
|
665
|
+
}
|
|
666
|
+
async discoverModels(provider, options = {}) {
|
|
667
|
+
const key = this.modelDiscoveryKey(provider);
|
|
668
|
+
const now = Date.now();
|
|
669
|
+
// A failed refresh must carry the last confirmed catalog forward.
|
|
670
|
+
const retainedConfirmation = this.modelDiscoveryCache.get(key)?.confirmed;
|
|
671
|
+
if (!options.forceRefresh) {
|
|
672
|
+
const cached = this.modelDiscoveryCache.get(key);
|
|
673
|
+
if (cached && cached.expiresAt > now) {
|
|
674
|
+
const cachedResult = normalizeProviderDiscoveryMetadata(provider, cached.result);
|
|
675
|
+
cached.result = cachedResult;
|
|
676
|
+
// A disk cache can outlive the application version that wrote it. Keep
|
|
677
|
+
// the current curated catalog authoritative over stale cached metadata,
|
|
678
|
+
// then retain cached remote-only ids for non-authoritative union results.
|
|
679
|
+
// This lets newly-added builtins surface immediately after an upgrade
|
|
680
|
+
// without waiting up to 24 hours or requiring a manual Ctrl+R refresh.
|
|
681
|
+
const local = this.localModelsForProvider(provider);
|
|
682
|
+
const localIds = new Set(local.map((model) => model.id));
|
|
683
|
+
const models = provider.id === "openai"
|
|
684
|
+
? cachedResult.models
|
|
685
|
+
: cachedResult.source === "static"
|
|
686
|
+
? local
|
|
687
|
+
: cachedResult.authoritative
|
|
688
|
+
? cachedResult.models
|
|
689
|
+
: [...local, ...cachedResult.models.filter((model) => !localIds.has(model.id))];
|
|
690
|
+
const result = { ...cachedResult, models, source: "cache" };
|
|
691
|
+
const current = this.getConfigured().find((item) => item.id === provider.id);
|
|
692
|
+
if (!current || this.modelDiscoveryKey(current) === key) {
|
|
693
|
+
this.applyDynamicDiscoveryMetadata(provider, result);
|
|
694
|
+
}
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
const inFlight = this.modelDiscoveryInFlight.get(key);
|
|
698
|
+
if (inFlight)
|
|
699
|
+
return inFlight;
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
// Keep the last confirmation readable while the replacement is in flight.
|
|
703
|
+
// Forced discovery bypasses freshness without invalidating a good catalog.
|
|
704
|
+
// Do not let a superseded request's finally-handler remove this retry.
|
|
705
|
+
this.modelDiscoveryInFlight.delete(key);
|
|
706
|
+
}
|
|
707
|
+
const generation = (this.modelDiscoveryGeneration.get(provider.id) ?? 0) + 1;
|
|
708
|
+
this.modelDiscoveryGeneration.set(provider.id, generation);
|
|
709
|
+
let pending;
|
|
710
|
+
pending = this.performModelDiscovery(provider).then((rawResult) => {
|
|
711
|
+
const result = normalizeProviderDiscoveryMetadata(provider, provider.id === "openai" && rawResult.error
|
|
712
|
+
? { ...rawResult, models: retainedConfirmation?.models ?? [] }
|
|
713
|
+
: rawResult);
|
|
714
|
+
if (!this.isCurrentModelDiscovery(provider, key, generation)) {
|
|
715
|
+
return {
|
|
716
|
+
models: provider.id === "openai" ? [] : this.localModelsForProvider(this.getConfigured().find((item) => item.id === provider.id) ?? provider),
|
|
717
|
+
source: "fallback",
|
|
718
|
+
authoritative: false,
|
|
719
|
+
error: "Model catalog changed while it was refreshing.",
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
this.applyDynamicDiscoveryMetadata(provider, result);
|
|
723
|
+
const previous = this.lastDiscoveryMembership.get(key);
|
|
724
|
+
const stored = { ...result, source: result.source === "cache" ? "remote" : result.source };
|
|
725
|
+
this.modelDiscoveryCache.set(key, {
|
|
726
|
+
result: stored,
|
|
727
|
+
expiresAt: Date.now() + (result.error ? MODEL_DISCOVERY_FAILURE_TTL_MS : MODEL_DISCOVERY_SUCCESS_TTL_MS),
|
|
728
|
+
// A complete success starts a new confirmation horizon; anything else
|
|
729
|
+
// (failure fallback, partial union) carries the previous one forward.
|
|
730
|
+
confirmed: isCompleteDiscovery(stored)
|
|
731
|
+
? { models: stored.models, until: Date.now() + MODEL_DISCOVERY_DISK_TTL_MS }
|
|
732
|
+
: this.modelDiscoveryCache.get(key)?.confirmed ?? retainedConfirmation,
|
|
733
|
+
identityKey: this.discoveryIdentity(provider),
|
|
734
|
+
providerId: provider.id,
|
|
735
|
+
authType: provider.authType,
|
|
736
|
+
protocol: provider.protocol,
|
|
737
|
+
});
|
|
738
|
+
if (!result.error)
|
|
739
|
+
this.saveDiscoveryDiskCache();
|
|
740
|
+
this.lastDiscoveryMembership.set(key, {
|
|
741
|
+
ids: result.models.map((model) => model.id),
|
|
742
|
+
authoritative: result.authoritative,
|
|
743
|
+
});
|
|
744
|
+
if (discoveryMembershipChanged(previous, result)) {
|
|
745
|
+
this.bumpRoutingRevision();
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
}).finally(() => {
|
|
749
|
+
if (this.modelDiscoveryInFlight.get(key) === pending) {
|
|
750
|
+
this.modelDiscoveryInFlight.delete(key);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
this.modelDiscoveryInFlight.set(key, pending);
|
|
754
|
+
return pending;
|
|
755
|
+
}
|
|
756
|
+
async performModelDiscovery(provider) {
|
|
757
|
+
const customModels = filterProviderModels(provider.id, this.modelConfig.getCustomModels(provider.id));
|
|
758
|
+
if (customModels.length > 0 && provider.id !== "openai") {
|
|
759
|
+
return { models: customModels, source: "static", authoritative: true };
|
|
760
|
+
}
|
|
761
|
+
if (provider.id === "openrouter") {
|
|
762
|
+
try {
|
|
763
|
+
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
|
764
|
+
signal: AbortSignal.timeout(MODEL_DISCOVERY_TIMEOUT_MS),
|
|
765
|
+
});
|
|
766
|
+
if (!response.ok)
|
|
767
|
+
throw new Error(`HTTP ${response.status}`);
|
|
768
|
+
const data = (await response.json());
|
|
769
|
+
const local = this.localModelsForProvider(provider);
|
|
770
|
+
const fallback = local.find((model) => model.id === OPENROUTER_MODEL_ID);
|
|
771
|
+
const remote = (data.data ?? []).find((model) => model?.id === OPENROUTER_MODEL_ID);
|
|
772
|
+
if (!remote || !fallback) {
|
|
773
|
+
return { models: local, source: "static", authoritative: true };
|
|
774
|
+
}
|
|
775
|
+
const reasoning = openRouterReasoningMetadata(remote.reasoning, fallback);
|
|
776
|
+
return {
|
|
777
|
+
models: [{
|
|
778
|
+
...fallback,
|
|
779
|
+
name: typeof remote.name === "string" && remote.name.trim() ? remote.name : fallback.name,
|
|
780
|
+
...reasoning,
|
|
781
|
+
contextWindow: typeof remote.context_length === "number" && remote.context_length > 0
|
|
782
|
+
? remote.context_length
|
|
783
|
+
: fallback.contextWindow,
|
|
784
|
+
}],
|
|
785
|
+
source: "remote",
|
|
786
|
+
authoritative: true,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
return this.fallbackDiscovery(provider, error);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (provider.id === "google" && provider.protocol === "ai-sdk" && provider.apiKey) {
|
|
794
|
+
try {
|
|
795
|
+
const descriptors = await fetchGeminiModels({
|
|
796
|
+
apiKey: provider.apiKey,
|
|
797
|
+
baseURL: provider.baseURL,
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
models: descriptors.map((descriptor) => {
|
|
801
|
+
const catalogEntry = getBuiltinModel("google", descriptor.id);
|
|
802
|
+
return {
|
|
803
|
+
id: descriptor.id,
|
|
804
|
+
name: descriptor.name,
|
|
805
|
+
providerId: provider.id,
|
|
806
|
+
reasoningLevels: descriptor.reasoningLevels,
|
|
807
|
+
defaultReasoningLevel: descriptor.defaultReasoningLevel ?? catalogEntry?.defaultReasoningLevel,
|
|
808
|
+
contextWindow: descriptor.contextWindow ?? catalogEntry?.contextWindow,
|
|
809
|
+
};
|
|
810
|
+
}),
|
|
811
|
+
source: "remote",
|
|
812
|
+
authoritative: true,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
return this.fallbackDiscovery(provider, error);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (provider.id === "openai" && provider.authType === "oauth" && provider.apiKey) {
|
|
820
|
+
try {
|
|
821
|
+
await this.prepareProvider(provider.id);
|
|
822
|
+
const currentProvider = this.getConfigured().find((item) => item.id === provider.id) ?? provider;
|
|
823
|
+
const catalog = await fetchOpenAICodexModelCatalog({
|
|
824
|
+
baseURL: currentProvider.baseURL,
|
|
825
|
+
accessToken: currentProvider.apiKey,
|
|
826
|
+
timeoutMs: OPENAI_CODEX_MODEL_DISCOVERY_TIMEOUT_MS,
|
|
827
|
+
});
|
|
828
|
+
if (catalog.status === "unavailable") {
|
|
829
|
+
throw new Error(`OpenAI Codex model catalog is unavailable${catalog.reason ? ` (${catalog.reason})` : ""}.`);
|
|
830
|
+
}
|
|
831
|
+
const visible = catalog.descriptors.filter((descriptor) => descriptor.visibility !== "hide");
|
|
832
|
+
return {
|
|
833
|
+
models: visible.map((descriptor) => {
|
|
834
|
+
const catalogEntry = getBuiltinModel("openai-codex", descriptor.id);
|
|
835
|
+
return {
|
|
836
|
+
id: descriptor.id,
|
|
837
|
+
name: descriptor.displayName || catalogEntry?.name || descriptor.id,
|
|
838
|
+
providerId: provider.id,
|
|
839
|
+
reasoningLevels: descriptor.reasoningLevels ?? catalogEntry?.reasoningLevels,
|
|
840
|
+
defaultReasoningLevel: descriptor.defaultReasoningLevel ?? catalogEntry?.defaultReasoningLevel,
|
|
841
|
+
contextWindow: descriptor.contextWindow ?? catalogEntry?.contextWindow,
|
|
842
|
+
useResponsesLite: descriptor.useResponsesLite ?? catalogEntry?.useResponsesLite,
|
|
843
|
+
toolOutputTokenLimit: descriptor.toolOutputTokenLimit ?? catalogEntry?.toolOutputTokenLimit,
|
|
844
|
+
};
|
|
845
|
+
}),
|
|
846
|
+
source: "remote",
|
|
847
|
+
authoritative: true,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
return this.fallbackDiscovery(provider, error);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// Grok subscription: the CLI chat proxy gates on the grok-cli identity
|
|
855
|
+
// headers and a fresh OAuth bearer, neither of which the generic path below
|
|
856
|
+
// sends. Discover through the refreshing subscription fetch, then merge the
|
|
857
|
+
// curated catalog with any remote-only ids so newly-released models surface
|
|
858
|
+
// without a code change. Authoritative because the merged list is what the
|
|
859
|
+
// routing catalog and model picker should treat as the account's catalog.
|
|
860
|
+
if (provider.id === "grok" && provider.authType === "oauth" && provider.apiKey) {
|
|
861
|
+
try {
|
|
862
|
+
await this.prepareProvider("grok");
|
|
863
|
+
const currentProvider = this.getConfigured().find((item) => item.id === "grok") ?? provider;
|
|
864
|
+
const grokAuth = this.createGrokAuthAdapter("grok");
|
|
865
|
+
if (!grokAuth)
|
|
866
|
+
throw new Error("Grok OAuth credentials are unavailable.");
|
|
867
|
+
const remote = await fetchGrokSubscriptionModels(currentProvider.baseURL, grokAuth);
|
|
868
|
+
const local = this.localModelsForProvider(currentProvider);
|
|
869
|
+
const known = new Set(local.map((model) => model.id));
|
|
870
|
+
const extras = remote
|
|
871
|
+
.filter((entry) => !known.has(entry.id) && isLikelyChatModelId(entry.id))
|
|
872
|
+
.map((entry) => {
|
|
873
|
+
const inferred = inferGrokModelMetadata(entry.id);
|
|
874
|
+
// Prefer the server-declared effort ladder and context window over
|
|
875
|
+
// id-based inference; fall back to inference only when the endpoint
|
|
876
|
+
// omitted them (or for ids that don't match a known family).
|
|
877
|
+
const levels = entry.reasoningEfforts && entry.reasoningEfforts.length > 0
|
|
878
|
+
? entry.reasoningEfforts
|
|
879
|
+
: inferred.levels;
|
|
880
|
+
const defaultLevel = entry.defaultReasoningEffort && levels.includes(entry.defaultReasoningEffort)
|
|
881
|
+
? entry.defaultReasoningEffort
|
|
882
|
+
: inferred.defaultLevel;
|
|
883
|
+
return {
|
|
884
|
+
id: entry.id,
|
|
885
|
+
name: entry.name || entry.id,
|
|
886
|
+
providerId: currentProvider.id,
|
|
887
|
+
reasoningLevels: levels,
|
|
888
|
+
...(defaultLevel ? { defaultReasoningLevel: defaultLevel } : {}),
|
|
889
|
+
...(entry.contextWindow !== undefined
|
|
890
|
+
? { contextWindow: entry.contextWindow }
|
|
891
|
+
: inferred.contextWindow !== undefined
|
|
892
|
+
? { contextWindow: inferred.contextWindow }
|
|
893
|
+
: {}),
|
|
894
|
+
};
|
|
895
|
+
})
|
|
896
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
897
|
+
return {
|
|
898
|
+
models: [...local, ...extras],
|
|
899
|
+
source: "remote",
|
|
900
|
+
authoritative: true,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
return this.fallbackDiscovery(provider, error);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// Generic OpenAI-compatible discovery: most vendors expose GET /models on
|
|
908
|
+
// the same base URL. Probed against every configured provider, their lists
|
|
909
|
+
// turned out to be neither complete nor clean — zhipuai omits glm-5.2,
|
|
910
|
+
// stepfun omits step-3.7-flash (both demonstrably usable), alibaba returns
|
|
911
|
+
// 236 entries including image/audio models, fireworks 412s. So the remote
|
|
912
|
+
// list AUGMENTS the curated catalog instead of replacing it: builtin
|
|
913
|
+
// entries always survive (they carry tier/reasoning metadata and some work
|
|
914
|
+
// while unlisted), remote-only ids are appended, obvious non-chat
|
|
915
|
+
// modalities are filtered out. Not authoritative — membership is a union,
|
|
916
|
+
// so nothing downstream may treat it as a closed allowlist.
|
|
917
|
+
if ((isOpenAICompatibleProtocol(provider.protocol) || provider.id === "openai") && provider.apiKey) {
|
|
918
|
+
try {
|
|
919
|
+
const remote = await fetchOpenAICompatibleModelIds(provider);
|
|
920
|
+
const local = this.localModelsForProvider(provider);
|
|
921
|
+
if (provider.id === "openai") {
|
|
922
|
+
const metadata = new Map(local.map(model => [model.id, model]));
|
|
923
|
+
return {
|
|
924
|
+
models: remote.filter(entry => isLikelyChatModelId(entry.id)).map(entry => ({
|
|
925
|
+
...metadata.get(entry.id), id: entry.id, name: entry.name || entry.id, providerId: provider.id,
|
|
926
|
+
})),
|
|
927
|
+
source: "remote", authoritative: true,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const known = new Set(local.map((model) => model.id));
|
|
931
|
+
const extras = remote
|
|
932
|
+
.filter((entry) => !known.has(entry.id) && isLikelyChatModelId(entry.id))
|
|
933
|
+
.map((entry) => ({
|
|
934
|
+
id: entry.id,
|
|
935
|
+
name: entry.name || entry.id,
|
|
936
|
+
providerId: provider.id,
|
|
937
|
+
}))
|
|
938
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
939
|
+
if (extras.length === 0) {
|
|
940
|
+
return { models: local, source: "static", authoritative: true };
|
|
941
|
+
}
|
|
942
|
+
return { models: [...local, ...extras], source: "remote", authoritative: false };
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
if (provider.id === "openai")
|
|
946
|
+
return this.fallbackDiscovery(provider, error);
|
|
947
|
+
// Vendors that don't implement /models (or gate it) fall back silently:
|
|
948
|
+
// the curated catalog is the contract, discovery is a bonus.
|
|
949
|
+
return { models: this.localModelsForProvider(provider), source: "static", authoritative: true };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
models: provider.id === "openai" ? [] : this.localModelsForProvider(provider),
|
|
954
|
+
source: "static",
|
|
955
|
+
authoritative: true,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
fallbackDiscovery(provider, error) {
|
|
959
|
+
return {
|
|
960
|
+
models: provider.id === "openai" ? [] : this.localModelsForProvider(provider),
|
|
961
|
+
source: "fallback",
|
|
962
|
+
authoritative: false,
|
|
963
|
+
error: modelDiscoveryError(error),
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
localModelsForProvider(provider) {
|
|
967
|
+
const customModels = filterProviderModels(provider.id, this.modelConfig.getCustomModels(provider.id));
|
|
968
|
+
if (customModels.length > 0)
|
|
969
|
+
return customModels;
|
|
970
|
+
const catalogProviderId = provider.id === "openai" && provider.authType === "oauth"
|
|
971
|
+
? "openai-codex"
|
|
972
|
+
: provider.id;
|
|
973
|
+
return listBuiltinModels(catalogProviderId).map((model) => ({
|
|
974
|
+
id: model.id,
|
|
975
|
+
name: model.name,
|
|
976
|
+
providerId: provider.id,
|
|
977
|
+
reasoningLevels: model.reasoningLevels,
|
|
978
|
+
defaultReasoningLevel: model.defaultReasoningLevel,
|
|
979
|
+
contextWindow: model.contextWindow,
|
|
980
|
+
tier: model.tier,
|
|
981
|
+
useResponsesLite: model.useResponsesLite,
|
|
982
|
+
toolOutputTokenLimit: model.toolOutputTokenLimit,
|
|
983
|
+
}));
|
|
984
|
+
}
|
|
985
|
+
discoveryIdentity(provider) {
|
|
986
|
+
if (provider.id === "openai" && provider.authType === "oauth") {
|
|
987
|
+
const credentials = this.authStorage.get(this.resolveOAuthAuthKey(provider.id));
|
|
988
|
+
return credentials?.accountId
|
|
989
|
+
|| extractChatGptAccountId(provider.apiKey)
|
|
990
|
+
|| "unknown-account";
|
|
991
|
+
}
|
|
992
|
+
if (provider.apiKey) {
|
|
993
|
+
return createHash("sha256").update(provider.apiKey).digest("hex").slice(0, 16);
|
|
994
|
+
}
|
|
995
|
+
return "anonymous";
|
|
996
|
+
}
|
|
997
|
+
modelDiscoveryKey(provider) {
|
|
998
|
+
return JSON.stringify([
|
|
999
|
+
provider.id,
|
|
1000
|
+
normalizeBaseURL(provider.baseURL),
|
|
1001
|
+
provider.authType ?? "api",
|
|
1002
|
+
provider.protocol ?? "default",
|
|
1003
|
+
this.discoveryIdentity(provider),
|
|
1004
|
+
provider.id === "openrouter"
|
|
1005
|
+
? OPENROUTER_CATALOG_SCOPE
|
|
1006
|
+
// The ChatGPT backend filters /codex/models by the client version we
|
|
1007
|
+
// claim, so a catalog fetched under an older pin (possibly by another
|
|
1008
|
+
// still-running Bubble sharing the disk cache) must not satisfy this
|
|
1009
|
+
// build's discovery.
|
|
1010
|
+
: provider.id === "openai"
|
|
1011
|
+
? `remote-only-v1:${provider.authType === "oauth" ? `codex-client:${getCodexClientVersion()}` : "api"}`
|
|
1012
|
+
: undefined,
|
|
1013
|
+
]);
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Read-only view of the cached discovery result for the provider's CURRENT
|
|
1017
|
+
* configuration/identity (docs/model-routing-design.md §1.4). Returns
|
|
1018
|
+
* undefined when nothing is cached or the cache has expired. `complete` is
|
|
1019
|
+
* derived strictly: only a successful full remote catalog qualifies —
|
|
1020
|
+
* never static or fallback results, whose internal `authoritative` flag
|
|
1021
|
+
* this design does not trust.
|
|
1022
|
+
* Desktop catalogs can explicitly retain the last confirmation beyond its
|
|
1023
|
+
* routing horizon; ordinary routing callers keep the default expiry policy.
|
|
1024
|
+
*/
|
|
1025
|
+
getCachedDiscoverySnapshot(providerId, options = {}) {
|
|
1026
|
+
const provider = this.getConfigured().find((item) => item.id === providerId);
|
|
1027
|
+
if (!provider)
|
|
1028
|
+
return undefined;
|
|
1029
|
+
const cached = this.modelDiscoveryCache.get(this.modelDiscoveryKey(provider));
|
|
1030
|
+
if (!cached)
|
|
1031
|
+
return undefined;
|
|
1032
|
+
const now = Date.now();
|
|
1033
|
+
const fresh = cached.expiresAt > now;
|
|
1034
|
+
if (fresh && isCompleteDiscovery(cached.result)) {
|
|
1035
|
+
return {
|
|
1036
|
+
models: cached.result.models,
|
|
1037
|
+
source: cached.result.source,
|
|
1038
|
+
complete: true,
|
|
1039
|
+
expiresAt: cached.expiresAt,
|
|
1040
|
+
identityKey: cached.identityKey,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
// The live result is stale or not complete: routing keeps the last
|
|
1044
|
+
// confirmed catalog for this identity until its horizon passes, so a
|
|
1045
|
+
// long session does not lose its tier candidates after the freshness
|
|
1046
|
+
// minute or across one failed refresh.
|
|
1047
|
+
if (cached.confirmed && (cached.confirmed.until > now || options.allowExpiredConfirmation)) {
|
|
1048
|
+
return {
|
|
1049
|
+
models: cached.confirmed.models,
|
|
1050
|
+
source: "cache",
|
|
1051
|
+
complete: true,
|
|
1052
|
+
expiresAt: cached.confirmed.until,
|
|
1053
|
+
identityKey: cached.identityKey,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (!fresh)
|
|
1057
|
+
return undefined;
|
|
1058
|
+
return {
|
|
1059
|
+
models: cached.result.models,
|
|
1060
|
+
source: cached.result.source,
|
|
1061
|
+
complete: false,
|
|
1062
|
+
expiresAt: cached.expiresAt,
|
|
1063
|
+
identityKey: cached.identityKey,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
isCurrentModelDiscovery(provider, key, generation) {
|
|
1067
|
+
if (this.modelDiscoveryGeneration.get(provider.id) !== generation)
|
|
1068
|
+
return false;
|
|
1069
|
+
const current = this.getConfigured().find((item) => item.id === provider.id);
|
|
1070
|
+
return !current || this.modelDiscoveryKey(current) === key;
|
|
1071
|
+
}
|
|
1072
|
+
applyDynamicDiscoveryMetadata(provider, result) {
|
|
1073
|
+
const dynamicProviderId = provider.id === "openai" && provider.authType === "oauth"
|
|
1074
|
+
? "openai-codex"
|
|
1075
|
+
: provider.id === "google" && provider.protocol === "ai-sdk"
|
|
1076
|
+
? "google"
|
|
1077
|
+
: provider.id === "grok"
|
|
1078
|
+
? "grok"
|
|
1079
|
+
: undefined;
|
|
1080
|
+
if (!dynamicProviderId)
|
|
1081
|
+
return;
|
|
1082
|
+
if (!result.authoritative) {
|
|
1083
|
+
clearDynamicModelMetadata(dynamicProviderId);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
replaceDynamicModelMetadata(dynamicProviderId, result.models.map((model) => ({
|
|
1087
|
+
id: model.id,
|
|
1088
|
+
name: model.name,
|
|
1089
|
+
providerId: dynamicProviderId,
|
|
1090
|
+
// A remote model with no trusted effort metadata still has to be
|
|
1091
|
+
// selectable: default it to the plain "off" level (no reasoning control)
|
|
1092
|
+
// rather than an empty ladder, which the model picker would render
|
|
1093
|
+
// "unavailable" and skip during arrow-key navigation.
|
|
1094
|
+
reasoningLevels: model.reasoningLevels ?? ["off"],
|
|
1095
|
+
defaultReasoningLevel: model.defaultReasoningLevel,
|
|
1096
|
+
contextWindow: model.contextWindow,
|
|
1097
|
+
useResponsesLite: model.useResponsesLite,
|
|
1098
|
+
toolOutputTokenLimit: model.toolOutputTokenLimit,
|
|
1099
|
+
})));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Remote Gemini discovery does not expose Bubble's reasoning ladder; that
|
|
1104
|
+
* metadata is inferred locally. Never trust a persisted inference across app
|
|
1105
|
+
* upgrades, or a corrected capability matrix can remain stale for the cache
|
|
1106
|
+
* TTL after restart.
|
|
1107
|
+
*/
|
|
1108
|
+
function normalizeProviderDiscoveryMetadata(provider, result) {
|
|
1109
|
+
if (provider.id !== "google" || provider.protocol !== "ai-sdk")
|
|
1110
|
+
return result;
|
|
1111
|
+
return {
|
|
1112
|
+
...result,
|
|
1113
|
+
models: result.models.map((model) => model.id.startsWith("gemini-")
|
|
1114
|
+
? { ...model, reasoningLevels: geminiReasoningLevels(model.id) }
|
|
1115
|
+
: model),
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function modelDiscoveryError(error) {
|
|
1119
|
+
if (error instanceof Error && error.message.trim())
|
|
1120
|
+
return error.message;
|
|
1121
|
+
return "Remote model catalog is unavailable.";
|
|
1122
|
+
}
|
|
1123
|
+
/** True when a discovery write changes the routing world: member ids or completeness. */
|
|
1124
|
+
function discoveryMembershipChanged(previous, next) {
|
|
1125
|
+
if (!previous)
|
|
1126
|
+
return true;
|
|
1127
|
+
if (previous.authoritative !== next.authoritative)
|
|
1128
|
+
return true;
|
|
1129
|
+
const previousIds = new Set(previous.ids);
|
|
1130
|
+
if (previousIds.size !== next.models.length)
|
|
1131
|
+
return true;
|
|
1132
|
+
return next.models.some((model) => !previousIds.has(model.id));
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Builtin defaults that were captured into stored profiles before a builtin's
|
|
1136
|
+
* baseURL moved. Exactly these values are treated as "not customized" and
|
|
1137
|
+
* follow the builtin to its new address (and thereby its new protocol);
|
|
1138
|
+
* genuinely custom URLs and profiles with an explicit protocol are untouched.
|
|
1139
|
+
*/
|
|
1140
|
+
const LEGACY_BUILTIN_BASE_URLS = {
|
|
1141
|
+
// google moved from the Gemini OpenAI-compat endpoint to the native API
|
|
1142
|
+
// when the "ai-sdk" protocol landed.
|
|
1143
|
+
google: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
1144
|
+
};
|
|
1145
|
+
function upgradeLegacyBaseURL(providerId, baseURL, explicitProtocol) {
|
|
1146
|
+
if (explicitProtocol)
|
|
1147
|
+
return baseURL;
|
|
1148
|
+
const legacy = LEGACY_BUILTIN_BASE_URLS[providerId];
|
|
1149
|
+
if (!legacy || normalizeBaseURL(baseURL) !== normalizeBaseURL(legacy))
|
|
1150
|
+
return baseURL;
|
|
1151
|
+
return getBuiltinProvider(providerId)?.baseURL ?? baseURL;
|
|
1152
|
+
}
|
|
1153
|
+
function resolveConfiguredProtocol(providerId, baseURL, explicitProtocol) {
|
|
1154
|
+
if (explicitProtocol)
|
|
1155
|
+
return explicitProtocol;
|
|
1156
|
+
const builtin = getBuiltinProvider(providerId);
|
|
1157
|
+
if (!builtin?.protocol)
|
|
1158
|
+
return undefined;
|
|
1159
|
+
const normalizedBaseURL = normalizeBaseURL(baseURL);
|
|
1160
|
+
if (!normalizedBaseURL || normalizedBaseURL === normalizeBaseURL(builtin.baseURL)) {
|
|
1161
|
+
return builtin.protocol;
|
|
1162
|
+
}
|
|
1163
|
+
if (normalizedBaseURL.includes("/anthropic")) {
|
|
1164
|
+
return "anthropic-messages";
|
|
1165
|
+
}
|
|
1166
|
+
return undefined;
|
|
1167
|
+
}
|
|
1168
|
+
function normalizeBaseURL(baseURL) {
|
|
1169
|
+
return baseURL.trim().replace(/\/+$/, "").toLowerCase();
|
|
1170
|
+
}
|
|
1171
|
+
/** Encode a model selection as "providerId:modelId". */
|
|
1172
|
+
export function encodeModel(providerId, modelId) {
|
|
1173
|
+
return `${providerId}:${modelId}`;
|
|
1174
|
+
}
|
|
1175
|
+
/** Decode "providerId:modelId" or legacy plain modelId. */
|
|
1176
|
+
export function decodeModel(value) {
|
|
1177
|
+
if (value.includes(":")) {
|
|
1178
|
+
const [providerId, ...rest] = value.split(":");
|
|
1179
|
+
return { providerId, modelId: rest.join(":") };
|
|
1180
|
+
}
|
|
1181
|
+
return { modelId: value };
|
|
1182
|
+
}
|
|
1183
|
+
/** Strip provider prefix for concise display. */
|
|
1184
|
+
export function displayModel(model) {
|
|
1185
|
+
const { providerId, modelId } = decodeModel(model);
|
|
1186
|
+
return providerId ? getBuiltinModel(providerId, modelId)?.name ?? modelId : modelId;
|
|
1187
|
+
}
|
|
1188
|
+
/** Normalize user input to provider:model format when possible. */
|
|
1189
|
+
export function normalizeModel(model, defaultProvider = "openai") {
|
|
1190
|
+
const { providerId, modelId } = decodeModel(model);
|
|
1191
|
+
if (providerId)
|
|
1192
|
+
return model;
|
|
1193
|
+
return encodeModel(defaultProvider, modelId);
|
|
1194
|
+
}
|