@maudecode/talaria-web 1.17.2 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +8 -8
- package/dist/api/auth-raw.d.ts +1 -1
- package/dist/api/auth-raw.js +20 -20
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +7 -1
- package/dist/api/automation-raw.js +74 -27
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +128 -23
- package/dist/api/automation-router.js +29 -13
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +1845 -40
- package/dist/api/chat-router.js +196 -114
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +37 -3
- package/dist/api/git-router.js +47 -48
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +9 -1
- package/dist/api/raw-routes.js +286 -73
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +16 -6
- package/dist/api/router.js +28 -26
- package/dist/api/router.js.map +1 -1
- package/dist/api/session-visibility.d.ts +4 -4
- package/dist/api/session-visibility.js +26 -16
- package/dist/api/session-visibility.js.map +1 -1
- package/dist/api/sessions-router.d.ts +3312 -131
- package/dist/api/sessions-router.js +287 -157
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +457 -8
- package/dist/api/settings-router.js +54 -33
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +7 -2
- package/dist/api/sse-routes.js +109 -62
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.d.ts +5 -0
- package/dist/api/tools-raw.js +63 -55
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +998 -85
- package/dist/api/tools-router.js +447 -61
- package/dist/api/tools-router.js.map +1 -1
- package/dist/api/transcribe-stream.d.ts +16 -0
- package/dist/api/transcribe-stream.js +172 -0
- package/dist/api/transcribe-stream.js.map +1 -0
- package/dist/app.d.ts +4 -1
- package/dist/app.js +98 -59
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +15 -11
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +5 -2
- package/dist/auth/oidc.js +8 -7
- package/dist/auth/oidc.js.map +1 -1
- package/dist/auth/passkeys.d.ts +15 -14
- package/dist/auth/passkeys.js +89 -91
- package/dist/auth/passkeys.js.map +1 -1
- package/dist/auth/store.d.ts +17 -7
- package/dist/auth/store.js +93 -52
- package/dist/auth/store.js.map +1 -1
- package/dist/bin/talaria-web.js +12 -81
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/ctl.js +2 -1
- package/dist/cli/ctl.js.map +1 -1
- package/dist/cli/dotenv.d.ts +5 -0
- package/dist/cli/dotenv.js +15 -2
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/cli/launcher.js +5 -5
- package/dist/cli/launcher.js.map +1 -1
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +85 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/supervise.d.ts +2 -0
- package/dist/cli/supervise.js +67 -4
- package/dist/cli/supervise.js.map +1 -1
- package/dist/config/agent-config.d.ts +34 -11
- package/dist/config/agent-config.js +116 -60
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +19 -6
- package/dist/config.js.map +1 -1
- package/dist/fs/atomic.d.ts +1 -0
- package/dist/fs/atomic.js +4 -1
- package/dist/fs/atomic.js.map +1 -1
- package/dist/fs/stat.d.ts +1 -0
- package/dist/fs/stat.js +11 -0
- package/dist/fs/stat.js.map +1 -0
- package/dist/http/context.d.ts +31 -12
- package/dist/http/context.js +60 -24
- package/dist/http/context.js.map +1 -1
- package/dist/http/csp.d.ts +2 -1
- package/dist/http/csp.js +3 -2
- package/dist/http/csp.js.map +1 -1
- package/dist/onboarding.d.ts +2 -2
- package/dist/onboarding.js +10 -8
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +20 -4
- package/dist/profiles/profiles.js +115 -30
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/projects.d.ts +15 -6
- package/dist/projects.js +55 -47
- package/dist/projects.js.map +1 -1
- package/dist/providers/catalog.d.ts +84 -14
- package/dist/providers/catalog.js +474 -151
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/env-file.d.ts +10 -3
- package/dist/providers/env-file.js +24 -75
- package/dist/providers/env-file.js.map +1 -1
- package/dist/providers/runtime-env.d.ts +4 -2
- package/dist/providers/runtime-env.js +9 -6
- package/dist/providers/runtime-env.js.map +1 -1
- package/dist/providers/tables.d.ts +3 -1
- package/dist/providers/tables.js +4 -2
- package/dist/providers/tables.js.map +1 -1
- package/dist/raster-image.d.ts +7 -0
- package/dist/raster-image.js +267 -0
- package/dist/raster-image.js.map +1 -0
- package/dist/redact.js +342 -46
- package/dist/redact.js.map +1 -1
- package/dist/release.d.ts +7 -0
- package/dist/release.js +14 -0
- package/dist/release.js.map +1 -1
- package/dist/runtime.d.ts +5 -1
- package/dist/runtime.js +305 -96
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +15 -0
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +9 -3
- package/dist/sessions/anchor.js +28 -36
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-events.d.ts +2 -0
- package/dist/sessions/background-events.js +10 -0
- package/dist/sessions/background-events.js.map +1 -0
- package/dist/sessions/background-tasks.d.ts +26 -27
- package/dist/sessions/background-tasks.js +89 -97
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/background-updates.d.ts +15 -8
- package/dist/sessions/background-updates.js +101 -25
- package/dist/sessions/background-updates.js.map +1 -1
- package/dist/sessions/cli-sessions.d.ts +27 -6
- package/dist/sessions/cli-sessions.js +152 -42
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +77 -9
- package/dist/sessions/completions.js +482 -45
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +8 -0
- package/dist/sessions/compress.js +2 -2
- package/dist/sessions/compress.js.map +1 -1
- package/dist/sessions/drafts.d.ts +14 -10
- package/dist/sessions/drafts.js +38 -43
- package/dist/sessions/drafts.js.map +1 -1
- package/dist/sessions/export.d.ts +5 -0
- package/dist/sessions/export.js +30 -0
- package/dist/sessions/export.js.map +1 -1
- package/dist/sessions/gateway-sessions.d.ts +46 -0
- package/dist/sessions/gateway-sessions.js +378 -0
- package/dist/sessions/gateway-sessions.js.map +1 -0
- package/dist/sessions/gateway-watcher.d.ts +7 -3
- package/dist/sessions/gateway-watcher.js +31 -19
- package/dist/sessions/gateway-watcher.js.map +1 -1
- package/dist/sessions/goals.d.ts +31 -0
- package/dist/sessions/goals.js +75 -0
- package/dist/sessions/goals.js.map +1 -0
- package/dist/sessions/handoff.d.ts +32 -0
- package/dist/sessions/handoff.js +114 -0
- package/dist/sessions/handoff.js.map +1 -0
- package/dist/sessions/journal.d.ts +5 -79
- package/dist/sessions/journal.js +15 -606
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +31 -16
- package/dist/sessions/list.js +139 -129
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +69 -13
- package/dist/sessions/merge.js +265 -64
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/notices.d.ts +22 -0
- package/dist/sessions/notices.js +72 -0
- package/dist/sessions/notices.js.map +1 -0
- package/dist/sessions/pending.d.ts +10 -4
- package/dist/sessions/pending.js +83 -7
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/prefix-index.d.ts +82 -0
- package/dist/sessions/prefix-index.js +232 -0
- package/dist/sessions/prefix-index.js.map +1 -0
- package/dist/sessions/recovery.d.ts +19 -0
- package/dist/sessions/recovery.js +322 -0
- package/dist/sessions/recovery.js.map +1 -0
- package/dist/sessions/relay.d.ts +38 -19
- package/dist/sessions/relay.js +302 -79
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +260 -55
- package/dist/sessions/service.js +1448 -462
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +24 -2
- package/dist/sessions/session.js +69 -7
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/shares.d.ts +7 -6
- package/dist/sessions/shares.js +22 -45
- package/dist/sessions/shares.js.map +1 -1
- package/dist/sessions/source-kind.js +1 -0
- package/dist/sessions/source-kind.js.map +1 -1
- package/dist/sessions/state-db-identity.d.ts +18 -0
- package/dist/sessions/state-db-identity.js +67 -0
- package/dist/sessions/state-db-identity.js.map +1 -0
- package/dist/sessions/state-db-worker.d.ts +1 -0
- package/dist/sessions/state-db-worker.js +35 -0
- package/dist/sessions/state-db-worker.js.map +1 -0
- package/dist/sessions/state-db.d.ts +113 -7
- package/dist/sessions/state-db.js +861 -44
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/storage/backend.d.ts +162 -0
- package/dist/sessions/storage/backend.js +19 -0
- package/dist/sessions/storage/backend.js.map +1 -0
- package/dist/sessions/storage/conformance.d.ts +20 -0
- package/dist/sessions/storage/conformance.js +237 -0
- package/dist/sessions/storage/conformance.js.map +1 -0
- package/dist/sessions/storage/document.d.ts +16 -0
- package/dist/sessions/storage/document.js +44 -0
- package/dist/sessions/storage/document.js.map +1 -0
- package/dist/sessions/storage/json-documents.d.ts +23 -0
- package/dist/sessions/storage/json-documents.js +233 -0
- package/dist/sessions/storage/json-documents.js.map +1 -0
- package/dist/sessions/storage/json-files.d.ts +12 -0
- package/dist/sessions/storage/json-files.js +124 -0
- package/dist/sessions/storage/json-files.js.map +1 -0
- package/dist/sessions/storage/json-installation.d.ts +3 -0
- package/dist/sessions/storage/json-installation.js +31 -0
- package/dist/sessions/storage/json-installation.js.map +1 -0
- package/dist/sessions/storage/json-journal.d.ts +114 -0
- package/dist/sessions/storage/json-journal.js +686 -0
- package/dist/sessions/storage/json-journal.js.map +1 -0
- package/dist/sessions/storage/json-recovery.d.ts +4 -0
- package/dist/sessions/storage/json-recovery.js +195 -0
- package/dist/sessions/storage/json-recovery.js.map +1 -0
- package/dist/sessions/storage/json.d.ts +68 -0
- package/dist/sessions/storage/json.js +258 -0
- package/dist/sessions/storage/json.js.map +1 -0
- package/dist/sessions/storage/row.d.ts +6 -0
- package/dist/sessions/storage/row.js +49 -0
- package/dist/sessions/storage/row.js.map +1 -0
- package/dist/sessions/storage/search-text.d.ts +4 -0
- package/dist/sessions/storage/search-text.js +16 -0
- package/dist/sessions/storage/search-text.js.map +1 -0
- package/dist/sessions/store.d.ts +65 -68
- package/dist/sessions/store.js +305 -602
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +4 -0
- package/dist/sessions/streams.js +9 -1
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.d.ts +2 -0
- package/dist/sessions/titles.js +5 -1
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/todo.d.ts +5 -0
- package/dist/sessions/todo.js +28 -21
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +5 -4
- package/dist/sessions/tool-display.js +30 -50
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +11 -2
- package/dist/sessions/turn-context.js +18 -11
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +122 -17
- package/dist/sessions/turn.js +939 -358
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +54 -0
- package/dist/sessions/wakeup-pause.js +163 -0
- package/dist/sessions/wakeup-pause.js.map +1 -0
- package/dist/sessions/window.js +13 -12
- package/dist/sessions/window.js.map +1 -1
- package/dist/settings.d.ts +19 -7
- package/dist/settings.js +178 -184
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/client.d.ts +17 -3
- package/dist/sidecar/client.js +78 -8
- package/dist/sidecar/client.js.map +1 -1
- package/dist/sidecar/discover.d.ts +2 -1
- package/dist/sidecar/discover.js +93 -10
- package/dist/sidecar/discover.js.map +1 -1
- package/dist/sidecar/env.d.ts +4 -0
- package/dist/sidecar/env.js +3 -0
- package/dist/sidecar/env.js.map +1 -0
- package/dist/sidecar/fake.d.ts +2 -2
- package/dist/sidecar/fake.js +19 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/startup.d.ts +13 -5
- package/dist/startup.js +48 -0
- package/dist/startup.js.map +1 -1
- package/dist/test/env-file.d.ts +5 -0
- package/dist/test/env-file.js +88 -0
- package/dist/test/env-file.js.map +1 -0
- package/dist/test/harness.d.ts +1 -0
- package/dist/test/harness.js +1 -1
- package/dist/test/harness.js.map +1 -1
- package/dist/test/passkeys.d.ts +22 -0
- package/dist/test/passkeys.js +62 -0
- package/dist/test/passkeys.js.map +1 -0
- package/dist/test/storage.d.ts +15 -0
- package/dist/test/storage.js +26 -0
- package/dist/test/storage.js.map +1 -0
- package/dist/test/ts-worker-hooks.d.ts +1 -0
- package/dist/test/ts-worker-hooks.js +24 -0
- package/dist/test/ts-worker-hooks.js.map +1 -0
- package/dist/text/diff.d.ts +9 -14
- package/dist/text/diff.js +74 -108
- package/dist/text/diff.js.map +1 -1
- package/dist/tools/commands.d.ts +6 -1
- package/dist/tools/commands.js +8 -2
- package/dist/tools/commands.js.map +1 -1
- package/dist/tools/connectors.d.ts +33 -0
- package/dist/tools/connectors.js +121 -0
- package/dist/tools/connectors.js.map +1 -0
- package/dist/tools/crons.d.ts +38 -1
- package/dist/tools/crons.js +122 -18
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +53 -0
- package/dist/tools/dashboard-plugins.js +188 -0
- package/dist/tools/dashboard-plugins.js.map +1 -0
- package/dist/tools/extensions.d.ts +18 -5
- package/dist/tools/extensions.js +344 -266
- package/dist/tools/extensions.js.map +1 -1
- package/dist/tools/gateway-restart.d.ts +62 -0
- package/dist/tools/gateway-restart.js +142 -0
- package/dist/tools/gateway-restart.js.map +1 -0
- package/dist/tools/health.d.ts +44 -0
- package/dist/tools/health.js +201 -46
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/hygiene.d.ts +4 -3
- package/dist/tools/hygiene.js +23 -9
- package/dist/tools/hygiene.js.map +1 -1
- package/dist/tools/kanban.d.ts +4 -2
- package/dist/tools/kanban.js +28 -19
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/mcp.d.ts +13 -0
- package/dist/tools/mcp.js +88 -0
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/memory-masks.d.ts +5 -0
- package/dist/tools/memory-masks.js +256 -0
- package/dist/tools/memory-masks.js.map +1 -0
- package/dist/tools/memory.d.ts +29 -3
- package/dist/tools/memory.js +69 -27
- package/dist/tools/memory.js.map +1 -1
- package/dist/tools/plugin-settings.d.ts +15 -0
- package/dist/tools/plugin-settings.js +90 -0
- package/dist/tools/plugin-settings.js.map +1 -0
- package/dist/tools/project-os.d.ts +11 -0
- package/dist/tools/project-os.js +267 -0
- package/dist/tools/project-os.js.map +1 -0
- package/dist/tools/skills.d.ts +14 -5
- package/dist/tools/skills.js +49 -16
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +34 -15
- package/dist/tools/update-notifications.js +141 -57
- package/dist/tools/update-notifications.js.map +1 -1
- package/dist/tools/update-rollback.d.ts +11 -0
- package/dist/tools/update-rollback.js +28 -0
- package/dist/tools/update-rollback.js.map +1 -0
- package/dist/tools/updates.d.ts +110 -32
- package/dist/tools/updates.js +1112 -294
- package/dist/tools/updates.js.map +1 -1
- package/dist/tools/wiki.d.ts +68 -0
- package/dist/tools/wiki.js +413 -0
- package/dist/tools/wiki.js.map +1 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +5 -0
- package/dist/util.js.map +1 -1
- package/dist/workspace/escape.d.ts +61 -0
- package/dist/workspace/escape.js +149 -0
- package/dist/workspace/escape.js.map +1 -0
- package/dist/workspace/extract.d.ts +22 -0
- package/dist/workspace/extract.js +308 -0
- package/dist/workspace/extract.js.map +1 -0
- package/dist/workspace/fs.d.ts +12 -0
- package/dist/workspace/fs.js +92 -23
- package/dist/workspace/fs.js.map +1 -1
- package/dist/workspace/git.d.ts +34 -1
- package/dist/workspace/git.js +328 -35
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +16 -13
- package/dist/workspace/media.js +43 -61
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/rollback.d.ts +36 -22
- package/dist/workspace/rollback.js +178 -272
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/unzip.js +4 -1
- package/dist/workspace/unzip.js.map +1 -1
- package/dist/workspace/upload.d.ts +7 -4
- package/dist/workspace/upload.js +5 -2
- package/dist/workspace/upload.js.map +1 -1
- package/dist/workspace/workspaces.d.ts +15 -19
- package/dist/workspace/workspaces.js +55 -127
- package/dist/workspace/workspaces.js.map +1 -1
- package/dist/workspace/worktrees.d.ts +2 -1
- package/dist/workspace/worktrees.js +179 -42
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +7 -3
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/agent_support.json +26 -0
- package/sidecar/scripts/record-fixtures.py +12 -3
- package/sidecar/scripts/replay_sidecar.py +32 -0
- package/sidecar/scripts/suite_runner.py +302 -0
- package/sidecar/scripts/test.sh +12 -4
- package/sidecar/talaria_sidecar/__init__.py +6 -1
- package/sidecar/talaria_sidecar/__main__.py +4 -2
- package/sidecar/talaria_sidecar/agent_mutation.py +975 -0
- package/sidecar/talaria_sidecar/agent_services.py +1053 -0
- package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
- package/sidecar/talaria_sidecar/home.py +56 -3
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +166 -24
- package/sidecar/talaria_sidecar/methods/chat.py +852 -135
- package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
- package/sidecar/talaria_sidecar/methods/commands.py +294 -4
- package/sidecar/talaria_sidecar/methods/config.py +123 -4
- package/sidecar/talaria_sidecar/methods/connectors.py +343 -0
- package/sidecar/talaria_sidecar/methods/cron.py +395 -36
- package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
- package/sidecar/talaria_sidecar/methods/gateway.py +212 -55
- package/sidecar/talaria_sidecar/methods/gateway_sessions.py +234 -0
- package/sidecar/talaria_sidecar/methods/goals.py +81 -41
- package/sidecar/talaria_sidecar/methods/kanban.py +108 -34
- package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
- package/sidecar/talaria_sidecar/methods/memory.py +84 -0
- package/sidecar/talaria_sidecar/methods/oauth.py +5 -2
- package/sidecar/talaria_sidecar/methods/process.py +218 -15
- package/sidecar/talaria_sidecar/methods/profiles.py +121 -13
- package/sidecar/talaria_sidecar/methods/providers.py +217 -28
- package/sidecar/talaria_sidecar/methods/runtime.py +69 -1
- package/sidecar/talaria_sidecar/methods/skills.py +67 -4
- package/sidecar/talaria_sidecar/methods/state_db.py +316 -75
- package/sidecar/talaria_sidecar/methods/stt.py +159 -78
- package/sidecar/talaria_sidecar/methods/text.py +14 -1
- package/sidecar/talaria_sidecar/methods/toolsets.py +49 -0
- package/sidecar/talaria_sidecar/methods/usage.py +113 -14
- package/sidecar/talaria_sidecar/methods/worktree.py +26 -19
- package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
- package/sidecar/talaria_sidecar/rpc.py +45 -4
- package/sidecar/talaria_sidecar/runtime.py +98 -13
- package/sidecar/talaria_sidecar/speed.py +35 -0
- package/static/dist/FILES.txt +126 -126
- package/static/dist/assets/AppShell-cey33v9s.js +8 -0
- package/static/dist/assets/ChatPage-i8wnugr7.js +18 -0
- package/static/dist/assets/ConnectTargets-hg19553x.js +1 -0
- package/static/dist/assets/ExtensionRoute-cly4apm5.js +1 -0
- package/static/dist/assets/Field-j8uqoigz.js +9 -0
- package/static/dist/assets/HubRoute-e4bgz41j.js +2 -0
- package/static/dist/assets/Markdown-lahuichm.js +598 -0
- package/static/dist/assets/RightPanel-lbo4i0fn.js +1 -0
- package/static/dist/assets/Select-gqatm2e0.js +1 -0
- package/static/dist/assets/SessionListPanel-mfqayfjh.js +1 -0
- package/static/dist/assets/SettingsLayout-b8980u7k.js +1 -0
- package/static/dist/assets/States-e7dkgtxf.js +1 -0
- package/static/dist/assets/Toaster-bfqqy786.js +1 -0
- package/static/dist/assets/_app-n1n6pfx1.js +1 -0
- package/static/dist/assets/{_app.ext._extensionId-opyh9y0v.js → _app.ext._extensionId-buxjxav3.js} +1 -1
- package/static/dist/assets/_app.index-jt8zwiu2.js +1 -0
- package/static/dist/assets/_app.insights-cv76ml71.js +1 -0
- package/static/dist/assets/_app.kanban-h6ts53j7.js +1 -0
- package/static/dist/assets/_app.logs-dm873z90.js +1 -0
- package/static/dist/assets/_app.memory-glk5y6m8.js +1 -0
- package/static/dist/assets/_app.profiles-ka6zpstq.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-bfd4h3id.js → _app.session._sessionId-g2vzghvc.js} +1 -1
- package/static/dist/assets/_app.settings-hgjdhzcd.js +1 -0
- package/static/dist/assets/_app.settings._section-jf2kmhae.js +3 -0
- package/static/dist/assets/_app.skills-cii852yq.js +1 -0
- package/static/dist/assets/_app.tasks-fp9q7atm.js +1 -0
- package/static/dist/assets/_app.todos-dw94qzue.js +1 -0
- package/static/dist/assets/_app.workspaces-inyygzze.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-b2x1lqot.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-h38o5l7b.js → architectureDiagram-5GKGNRK7-nwb3xovn.js} +1 -1
- package/static/dist/assets/archive-b1j004vn.js +1 -0
- package/static/dist/assets/back-g74gkozu.js +1 -0
- package/static/dist/assets/{blockDiagram-I7D4REHJ-bqquxv2u.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-f3fb8w6z.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
- package/static/dist/assets/channel-es95o4y8.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-bzfyyzp3.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-ly99rfx5.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-bg6ecfqf.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-e8k7mmqw.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-cmalnoaj.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-gmif5o01.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-f1nwwh6u.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-oktfh5v7.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-b1sdx96z.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-fhu4nzxt.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-em31rnef.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-bxhhyftw.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-fmrvwcav.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-lymukl2j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-lmza6k23.js → chunk-TICWLB2K-bnk3m1mq.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-kmihoqxh.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-kmihoqxh.js +1 -0
- package/static/dist/assets/copied-bli9tbcx.js +1 -0
- package/static/dist/assets/{copy-fw5b1pc6.js → copy-jamtd1pf.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-czlhbyhs.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-gq7mvswb.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-cqdbf7dr.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-o6arxa58.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-nhnd68tz.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-dk2sclzn.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-n3k5nyx3.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-m6q9cwdh.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-lsa5641p.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fo2ckbe6.js → erDiagram-RLTQ6QDP-brdu9lcm.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-njk1j8wl.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-nma6f0ga.js +1 -0
- package/static/dist/assets/folder-plus-lvt9kudb.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-nzqz6x9g.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-gukvcxe6.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-lqhdp8f0.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-igw6cp39.js → highlighted-body-KPVGNVTW-ghoavj8d.js} +1 -1
- package/static/dist/assets/host-hzm8n85n.js +1 -0
- package/static/dist/assets/index-drdcaqeq.css +2 -0
- package/static/dist/assets/index-jzm844he.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-gvu7a93l.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-m8t8g6w1.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-l07vkbgj.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-hybkgj8l.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-by13uvtn.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
- package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
- package/static/dist/assets/{line-osxq6fq7.js → line-h0ojzsvs.js} +1 -1
- package/static/dist/assets/link-k01ryaf9.js +1 -0
- package/static/dist/assets/login-bsae49ew.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-ihl34s74.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-i91oxmou.js → mermaid-parser.core-bnre27u8.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-m82mx8ww.js → mindmap-definition-YA3MSWOX-o1vathns.js} +1 -1
- package/static/dist/assets/onboarding-c65tysya.js +1 -0
- package/static/dist/assets/onboarding_model_label-gpn28uix.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-nbtgge2o.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
- package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
- package/static/dist/assets/{pegDiagram-XKGWAZYB-f1zxzga1.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-of8vuhxs.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-o7y8ftio.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-iqiwia1t.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-delnsy2v.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-nspnyq9u.js → railroad-74A4TZTK-a7t5eibp.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-ol8cdktf.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-i8vzd429.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-nw3ulewz.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-z41tqx5z.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
- package/static/dist/assets/{refresh-cw-nt1dtsx4.js → refresh-cw-mqbkv2a3.js} +1 -1
- package/static/dist/assets/refresh-l29ljp76.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-bdvif194.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-h73e0rol.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
- package/static/dist/assets/saved-ndd4t2xb.js +1 -0
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-iv95mb1z.js → sequenceDiagram-WJ2MYXX4-i0v116q0.js} +2 -2
- package/static/dist/assets/settings_section_extensions_title-jhfo6o8z.js +1 -0
- package/static/dist/assets/share._token-c1ri0hy1.js +2 -0
- package/static/dist/assets/sliders-horizontal-j3jjt9tv.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-k417ixmf.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-esola70w.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-getrz2y8.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
- package/static/dist/assets/toast-i3h34v6o.js +64 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-hc7ap1q7.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-b7k1syy5.js → treemap-WGGIJYW6-lzr49vv5.js} +1 -1
- package/static/dist/assets/untitled-oaoya4qj.js +1 -0
- package/static/dist/assets/useForm-nfaxgmsk.js +1 -0
- package/static/dist/assets/{useParams-lhg566ew.js → useParams-gjc70qru.js} +1 -1
- package/static/dist/assets/{useSearch-lbrh6ejn.js → useSearch-e6ae539u.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-n6pcy5qb.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-lpa18vz1.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-kpclwwg7.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-paa2n8y0.js → xychartDiagram-S5SC5T6Z-aqbgvujg.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-fb2td9zi.js +0 -1
- package/static/dist/assets/ChatPage-cj96kiy9.js +0 -25
- package/static/dist/assets/Dialog-kxcg7ard.js +0 -1
- package/static/dist/assets/ExtensionRoute-jf7h0754.js +0 -1
- package/static/dist/assets/HubRoute-gmanpd5y.js +0 -2
- package/static/dist/assets/Markdown-mdwlvj3w.js +0 -598
- package/static/dist/assets/RightPanel-ftvrjlcp.js +0 -1
- package/static/dist/assets/Select-bilaypej.js +0 -1
- package/static/dist/assets/SessionListPanel-og8n36qs.js +0 -1
- package/static/dist/assets/SettingsLayout-jonjefmj.js +0 -1
- package/static/dist/assets/States-nmejrwrm.js +0 -1
- package/static/dist/assets/Toaster-nnl8zkt9.js +0 -1
- package/static/dist/assets/_app-gdidzd94.js +0 -1
- package/static/dist/assets/_app.index-hbarzbjx.js +0 -1
- package/static/dist/assets/_app.insights-luywlarz.js +0 -1
- package/static/dist/assets/_app.kanban-kincnbr8.js +0 -1
- package/static/dist/assets/_app.logs-oi2gr3gs.js +0 -1
- package/static/dist/assets/_app.memory-b6jfivt6.js +0 -1
- package/static/dist/assets/_app.profiles-lpvixmqp.js +0 -1
- package/static/dist/assets/_app.settings-vowx3iq0.js +0 -1
- package/static/dist/assets/_app.settings._section-cnntg1tu.js +0 -3
- package/static/dist/assets/_app.skills-fb57lpau.js +0 -1
- package/static/dist/assets/_app.tasks-eszpx1q7.js +0 -2
- package/static/dist/assets/_app.todos-nr05ac6w.js +0 -1
- package/static/dist/assets/_app.workspaces-d2luomu6.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +0 -1
- package/static/dist/assets/back-bemaer09.js +0 -1
- package/static/dist/assets/cancel-drd0g6d9.js +0 -1
- package/static/dist/assets/channel-cpdgf867.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +0 -1
- package/static/dist/assets/copied-ima3pysb.js +0 -1
- package/static/dist/assets/endpoints-d33siokx.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +0 -1
- package/static/dist/assets/index-cxq58xb1.js +0 -11
- package/static/dist/assets/index-hgtx08n5.css +0 -2
- package/static/dist/assets/lazyRouteComponent-af0lvgjx.js +0 -1
- package/static/dist/assets/link-jyxicdhg.js +0 -1
- package/static/dist/assets/login-ec21t39x.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +0 -1
- package/static/dist/assets/onboarding-btnltvyz.js +0 -1
- package/static/dist/assets/onboarding_model_label-dp7xffoh.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +0 -1
- package/static/dist/assets/refresh-b4uams1i.js +0 -1
- package/static/dist/assets/saved-kg52csy7.js +0 -1
- package/static/dist/assets/settings_section_extensions_title-ej0iapxe.js +0 -1
- package/static/dist/assets/share._token-f97e94r0.js +0 -2
- package/static/dist/assets/sliders-horizontal-fj18it47.js +0 -1
- package/static/dist/assets/sse-fwpqtzbg.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +0 -8
- package/static/dist/assets/todoStore-eginf9h4.js +0 -1
- package/static/dist/assets/tts-lxdxnmjd.js +0 -1
- package/static/dist/assets/untitled-ovpfthg4.js +0 -1
- package/static/dist/assets/useForm-bcblv1rx.js +0 -1
- package/static/dist/assets/useLocale-fi5jb8ma.js +0 -1
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
* Provider cards, the model picker catalog, live model lists, quotas and cost
|
|
3
|
-
* history (Python `api/providers.py`, the `/api/models` parts of
|
|
4
|
-
* `api/config.py`, `_handle_live_models`).
|
|
5
|
-
*
|
|
6
|
-
* The Agent's own primitives (registry, auth status, live model ids, account
|
|
7
|
-
* usage) come through the sidecar; the static tables and key detection live
|
|
8
|
-
* here. ponytail: one live-id cache with a 24h TTL replaces the Python
|
|
9
|
-
* publisher/provenance machinery; `refresh()` evicts it.
|
|
10
|
-
*/
|
|
1
|
+
import { updateDocument } from '../sessions/storage/document.js';
|
|
11
2
|
import { homeDotenvKeys } from '../cli/dotenv.js';
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
13
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
14
5
|
import { join } from 'node:path';
|
|
15
6
|
import { atomicWriteText } from '../fs/atomic.js';
|
|
@@ -17,7 +8,7 @@ import { str } from '../util.js';
|
|
|
17
8
|
import { QUOTA_THRESHOLD_DEFAULTS } from '../settings.js';
|
|
18
9
|
import { loadEnvFile } from './env-file.js';
|
|
19
10
|
import { ACCOUNT_USAGE_PROVIDERS, FALLBACK_MODELS, OAUTH_PROVIDERS, PORTAL_PROVIDERS, PROVIDER_DISPLAY, PROVIDER_ENV_VAR, PROVIDER_ENV_VAR_ALIASES, PROVIDER_MODELS, SELF_HOSTED_PROVIDER_IDS, } from './tables.js';
|
|
20
|
-
import { activeProviderFromConfig, canonicaliseProviderId, configuredModelIds, configuredModelOptions, customProviderEntries, customProviderSlug, dict, effectiveDefaultModel, isDict,
|
|
11
|
+
import { activeProviderFromConfig, canonicaliseProviderId, configuredModelIds, configuredModelOptions, customProviderEntries, customProviderSlug, dict, effectiveDefaultModel, isDict, modelSection, parseProviderQualifiedModel, providerIdentity, resolveProviderAlias, } from '../config/agent-config.js';
|
|
21
12
|
/** Python `_model_matches_picker_selection`: same bare id, and the routing hints agree when the selection names one. */
|
|
22
13
|
function modelMatchesPickerSelection(modelId, selected, providerId) {
|
|
23
14
|
const candidate = modelId.trim();
|
|
@@ -133,16 +124,19 @@ export function stampAuxiliarySelections(aux, catalog) {
|
|
|
133
124
|
const match = provider === 'auto' ? undefined : options.find((o) => o.provider === key && o.bare === model);
|
|
134
125
|
if (match)
|
|
135
126
|
return { value_label: match.label || match.bare, provider_label: match.group, option_id: match.id };
|
|
136
|
-
|
|
137
|
-
return { value_label: model || null, provider_label: provider === 'auto' ? null : group?.provider ?? displayName(provider), option_id: null };
|
|
127
|
+
return { value_label: model || null, provider_label: provider === 'auto' ? null : providerLabel(catalog, provider), option_id: null };
|
|
138
128
|
};
|
|
139
129
|
// Auto falls back to the main chat model; name the effective one the catalog resolved (a legacy string `model` or a
|
|
140
130
|
// `HERMES_MODEL`-style override never reaches `aux.main`).
|
|
141
131
|
const mainParsed = parseProviderQualifiedModel(catalog.default_model);
|
|
142
132
|
const main = describe(mainParsed?.[1] ?? catalog.active_provider ?? aux.main.provider, mainParsed?.[0] ?? (catalog.default_model || aux.main.model));
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
133
|
+
const isAuto = (route) => (str(route.provider).trim() || 'auto') === 'auto' && !str(route.model).trim();
|
|
134
|
+
const tasks = aux.tasks.map(({ effective, ...t }) => {
|
|
135
|
+
if (isAuto(t)) {
|
|
136
|
+
// TAL-800: an Auto task that inherits another slot runs on that slot's route, the main model only when it is Auto too.
|
|
137
|
+
const base = isDict(effective) && !isAuto(effective) ? describe(effective.provider, effective.model) : main;
|
|
138
|
+
return { ...t, is_auto: true, value_label: base.value_label, provider_label: base.provider_label, selected_option_id: null, in_catalog: true };
|
|
139
|
+
}
|
|
146
140
|
const pinned = describe(t.provider, t.model);
|
|
147
141
|
return { ...t, is_auto: false, value_label: pinned.value_label, provider_label: pinned.provider_label, selected_option_id: pinned.option_id, in_catalog: pinned.option_id !== null };
|
|
148
142
|
});
|
|
@@ -156,6 +150,9 @@ const configuredBaseUrl = (pid, config) => {
|
|
|
156
150
|
const cfg = dict(providers[pid] ?? providers[Object.keys(providers).find((k) => providerIdentity(k) === providerIdentity(pid)) ?? '']);
|
|
157
151
|
return str(cfg.base_url).trim() || (activeProviderFromConfig(config) === pid ? str(modelSection(config).base_url).trim() : '');
|
|
158
152
|
};
|
|
153
|
+
/** TAL-603: the credential family a card's badge names; `null` when the card holds no credential. */
|
|
154
|
+
const KEY_SOURCE_KINDS = { env_file: 'env', env_var: 'env', env: 'env', oauth: 'oauth', token: 'oauth', config_yaml: 'config', config: 'config', credential_pool: 'pool', plugin: 'plugin' };
|
|
155
|
+
const keySourceKind = (row) => row.has_key === true && row.key_source !== 'none' ? KEY_SOURCE_KINDS[str(row.key_source)] ?? 'other' : null;
|
|
159
156
|
/** TAL-570: Ollama and LM Studio run keyless, so a configured endpoint makes them ready the way a key does. */
|
|
160
157
|
const selfHostedEndpoint = (pid, config) => SELF_HOSTED_PROVIDER_IDS.has(pid) && Boolean(configuredBaseUrl(pid, config));
|
|
161
158
|
/** Python `_unqualified_model_id`: strip a picker routing hint (`@provider:`). */
|
|
@@ -164,7 +161,13 @@ const unqualifiedModelId = (id) => parseProviderQualifiedModel(id)?.[0] ?? id.tr
|
|
|
164
161
|
const MODEL_PICKER_OVERFLOW_THRESHOLD = 25;
|
|
165
162
|
const MODEL_PICKER_VISIBLE_TARGET = 15;
|
|
166
163
|
const ACCOUNT_USAGE_CACHE_TTL_S = 45;
|
|
164
|
+
/** TAL-807: the oldest account-usage read the picker still shows a quota level from. */
|
|
165
|
+
const PICKER_USAGE_MAX_AGE_S = 600;
|
|
166
|
+
/** TAL-807: how long a picker read waits for a provider's first account-usage read. */
|
|
167
|
+
const PICKER_USAGE_WAIT_MS = 2_000;
|
|
167
168
|
const LIVE_TTL_S = 86_400;
|
|
169
|
+
/** TAL-799: the Agent's `_PROVIDER_MODELS_FALLBACK_TTL`; its placeholder for an unreachable catalog is re-probed soon. */
|
|
170
|
+
const FALLBACK_TTL_S = 60;
|
|
168
171
|
const PROVIDERS_TTL_S = 30;
|
|
169
172
|
/** The sidecar reads a balance endpoint with a 15 s timeout of its own. */
|
|
170
173
|
const BALANCE_TIMEOUT_MS = 20_000;
|
|
@@ -340,6 +343,11 @@ export function classifyQuotaSource(source, t) {
|
|
|
340
343
|
urgency: { remaining: urgency(source.windows[0]).remaining, pace: urgency(source.windows[source.pace_window_index ?? 0]).pace },
|
|
341
344
|
};
|
|
342
345
|
}
|
|
346
|
+
/** A provider's name as the catalog lists it (a named custom endpoint's configured name, a plugin's display name), else its static display name. */
|
|
347
|
+
export function providerLabel(catalog, provider) {
|
|
348
|
+
const key = canonicaliseProviderId(provider);
|
|
349
|
+
return catalog?.groups.find((g) => canonicaliseProviderId(g.provider_id) === key)?.provider ?? displayName(provider);
|
|
350
|
+
}
|
|
343
351
|
export function displayName(pid) {
|
|
344
352
|
return PROVIDER_DISPLAY[pid] ?? pid.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ');
|
|
345
353
|
}
|
|
@@ -388,6 +396,10 @@ function looksLikeCodexOauthToken(value) {
|
|
|
388
396
|
return false;
|
|
389
397
|
}
|
|
390
398
|
}
|
|
399
|
+
const BALANCE_PROVIDERS = ['openrouter', 'deepseek', 'opencode-go'];
|
|
400
|
+
function isBalanceProvider(pid) {
|
|
401
|
+
return BALANCE_PROVIDERS.some((provider) => provider === pid);
|
|
402
|
+
}
|
|
391
403
|
function valueCountsAsApiKey(pid, value) {
|
|
392
404
|
const text = str(value).trim();
|
|
393
405
|
if (!text)
|
|
@@ -455,9 +467,12 @@ export function uniqueQuotaSources(sources) {
|
|
|
455
467
|
}
|
|
456
468
|
export class ProviderCatalog {
|
|
457
469
|
deps;
|
|
470
|
+
scopedKeyRevisions = new Map();
|
|
471
|
+
scopedKeyAuthority = new Map();
|
|
472
|
+
credentialReads = new Map();
|
|
458
473
|
/** Failed live lookups by home, provider, and the `sourceFingerprint` they ran under (TAL-542). */
|
|
459
474
|
liveFailed = new Set();
|
|
460
|
-
/** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). */
|
|
475
|
+
/** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). `ttl`: seconds they stay fresh. */
|
|
461
476
|
liveIds = new Map();
|
|
462
477
|
liveInflight = new Map();
|
|
463
478
|
/** `pooled`: the fill's `poolProviders` answer, null when unknown (TAL-638). */
|
|
@@ -479,15 +494,23 @@ export class ProviderCatalog {
|
|
|
479
494
|
}
|
|
480
495
|
return { groups: this.liveIds.size, models, age_seconds: oldest === null ? null : Math.max(0, Math.round(this.deps.now() - oldest)) };
|
|
481
496
|
}
|
|
482
|
-
invalidate(profileHome, provider) {
|
|
497
|
+
invalidate(profileHome, provider, opts = {}) {
|
|
483
498
|
for (const key of [...this.liveIds.keys()]) {
|
|
484
499
|
if (profileHome && !key.startsWith(`${profileHome}\0`))
|
|
485
500
|
continue;
|
|
486
501
|
if (provider && !key.endsWith(`\0${provider}`))
|
|
487
502
|
continue;
|
|
488
|
-
|
|
503
|
+
if (opts.preserveConfirmed)
|
|
504
|
+
this.liveIds.get(key).ttl = 0;
|
|
505
|
+
else
|
|
506
|
+
this.liveIds.delete(key);
|
|
489
507
|
}
|
|
490
508
|
this.providersCache.clear();
|
|
509
|
+
if (opts.preserveConfirmed)
|
|
510
|
+
return;
|
|
511
|
+
for (const key of [...this.lastQuotaSources.keys()])
|
|
512
|
+
if (!profileHome || key.startsWith(`${profileHome}\0`))
|
|
513
|
+
this.lastQuotaSources.delete(key);
|
|
491
514
|
// Credentials changed: the last pool answers no longer stand in for a failed lookup.
|
|
492
515
|
for (const key of [...this.lastPoolAnswers.keys()])
|
|
493
516
|
if (!profileHome || key.startsWith(`${profileHome}\0`))
|
|
@@ -511,27 +534,44 @@ export class ProviderCatalog {
|
|
|
511
534
|
}
|
|
512
535
|
return catalogOptionId(catalog, model, provider);
|
|
513
536
|
}
|
|
537
|
+
/** TAL-821: `providerLabel` against the last catalog built for this home; a cold home starts building one. */
|
|
538
|
+
providerLabel(profileHome, provider) {
|
|
539
|
+
const catalog = this.lastModels.get(profileHome);
|
|
540
|
+
if (!catalog)
|
|
541
|
+
void this.warmModelOptions(profileHome);
|
|
542
|
+
return providerLabel(catalog, provider);
|
|
543
|
+
}
|
|
514
544
|
/**
|
|
515
545
|
* TAL-542: the identity of the files a catalog and its live ids derive from: config.yaml, `.env`, the Agent's sign-in
|
|
516
546
|
* store (`auth.json`, rewritten by an account switch), and the installed plugins directory, so a change outside Web
|
|
517
|
-
* makes both stale.
|
|
547
|
+
* makes both stale. Opaque scoped-key revisions cover external secret sources without exposing their keys.
|
|
518
548
|
*/
|
|
519
|
-
sourceFingerprint(profileHome) {
|
|
549
|
+
sourceFingerprint(profileHome, includeScoped = true) {
|
|
520
550
|
const stamp = (name) => {
|
|
521
551
|
try {
|
|
522
552
|
const st = statSync(join(profileHome, name), { bigint: true });
|
|
523
|
-
return `${String(st.mtimeNs)}:${String(st.size)}:${String(st.ino)}`;
|
|
553
|
+
return `${String(st.mtimeNs)}:${String(st.ctimeNs)}:${String(st.size)}:${String(st.ino)}`;
|
|
524
554
|
}
|
|
525
555
|
catch {
|
|
526
556
|
return 'missing';
|
|
527
557
|
}
|
|
528
558
|
};
|
|
529
|
-
|
|
559
|
+
const names = new Set([...Object.values(PROVIDER_ENV_VAR), ...Object.values(PROVIDER_ENV_VAR_ALIASES).flat()]);
|
|
560
|
+
const config = this.deps.config.peek(profileHome, { refresh: false }) ?? {};
|
|
561
|
+
for (const cp of [modelSection(config), ...Object.values(dict(config.providers)).filter(isDict), ...customProviderEntries(config)]) {
|
|
562
|
+
for (const name of [str(cp.key_env).trim(), /^\$\{([^}]+)\}$/.exec(str(cp.api_key).trim())?.[1]])
|
|
563
|
+
if (name)
|
|
564
|
+
names.add(name);
|
|
565
|
+
}
|
|
566
|
+
const credentials = [...names].sort().map((name) => [name, this.processEnv(name, profileHome)]);
|
|
567
|
+
const models = ['HERMES_MODEL', 'OPENAI_MODEL', 'LLM_MODEL'].map((name) => [name, this.deps.env[name]]);
|
|
568
|
+
const environment = createHash('sha256').update(JSON.stringify([credentials, models, includeScoped ? this.scopedKeyRevisions.get(profileHome) ?? '[]' : '[]'])).digest('hex');
|
|
569
|
+
return [profileHome, environment, this.deps.config.fingerprint(profileHome), ...['.env', 'auth.json', 'plugins'].map(stamp)].join('|');
|
|
530
570
|
}
|
|
531
571
|
/** The last catalog built for this home, younger than `maxAgeS`, while its source files are unchanged since (TAL-542). */
|
|
532
572
|
currentModels(profileHome, maxAgeS = Infinity) {
|
|
533
573
|
const meta = this.lastModelsMeta.get(profileHome);
|
|
534
|
-
return meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
|
|
574
|
+
return this.scopedKeyAuthority.get(profileHome) !== false && meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
|
|
535
575
|
}
|
|
536
576
|
/**
|
|
537
577
|
* TAL-542: `repairSessionModel` against this home's catalog. Agent-owned sign-in and plugin state has no file to
|
|
@@ -551,6 +591,7 @@ export class ProviderCatalog {
|
|
|
551
591
|
* await. A source that changed while the build awaited makes that build stale, so it is rebuilt once.
|
|
552
592
|
*/
|
|
553
593
|
async warmSessionModelRepair(profileHome) {
|
|
594
|
+
await this.poolProviders(profileHome);
|
|
554
595
|
for (let attempt = 0; attempt < 2 && !this.currentModels(profileHome, PROVIDERS_TTL_S); attempt += 1) {
|
|
555
596
|
try {
|
|
556
597
|
await this.models(profileHome);
|
|
@@ -562,6 +603,7 @@ export class ProviderCatalog {
|
|
|
562
603
|
}
|
|
563
604
|
/** Builds the catalog `modelOptionFor` reads once per home, again after config.yaml or `.env` changes; an unavailable catalog leaves every option id null. */
|
|
564
605
|
async warmModelOptions(profileHome) {
|
|
606
|
+
await this.poolProviders(profileHome);
|
|
565
607
|
if (this.currentModels(profileHome))
|
|
566
608
|
return;
|
|
567
609
|
try {
|
|
@@ -573,16 +615,20 @@ export class ProviderCatalog {
|
|
|
573
615
|
* The process-environment value of `name` as a named profile may see it: values `loadStartupEnv` copied from the
|
|
574
616
|
* default profile's `.env` belong to that profile only, so they never count for another profile.
|
|
575
617
|
*/
|
|
576
|
-
|
|
577
|
-
|
|
618
|
+
/** `cleared`: the variable a key delete clears, which also retires its `.env`-owned startup copy (TAL-641). */
|
|
619
|
+
processEnv(name, profileHome, cleared) {
|
|
620
|
+
if ((name === cleared || !this.deps.isRootProfileHome(profileHome)) && homeDotenvKeys(this.deps.env).has(name))
|
|
578
621
|
return undefined;
|
|
579
622
|
return this.deps.env[name];
|
|
580
623
|
}
|
|
581
|
-
/**
|
|
582
|
-
|
|
624
|
+
/**
|
|
625
|
+
* Python `_provider_has_key` minus the credential pool, which `probeKey` reads from the sidecar.
|
|
626
|
+
* `cleared` (TAL-641) answers for after `setProviderKey(…, null)`: that variable's `.env` entry and startup copy are gone.
|
|
627
|
+
*/
|
|
628
|
+
providerHasKey(pid, config, envValues, profileHome, cleared) {
|
|
583
629
|
const envVar = providerEnvVar(pid);
|
|
584
630
|
if (envVar) {
|
|
585
|
-
if (valueCountsAsApiKey(pid, envValues[envVar]) || valueCountsAsApiKey(pid, this.processEnv(envVar, profileHome)))
|
|
631
|
+
if ((envVar !== cleared && valueCountsAsApiKey(pid, envValues[envVar])) || valueCountsAsApiKey(pid, this.processEnv(envVar, profileHome, cleared)))
|
|
586
632
|
return true;
|
|
587
633
|
for (const alias of PROVIDER_ENV_VAR_ALIASES[pid] ?? [])
|
|
588
634
|
if (valueCountsAsApiKey(pid, envValues[alias]) || valueCountsAsApiKey(pid, this.processEnv(alias, profileHome)))
|
|
@@ -606,25 +652,24 @@ export class ProviderCatalog {
|
|
|
606
652
|
return false;
|
|
607
653
|
}
|
|
608
654
|
async authStatus(profileHome, pid) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return
|
|
612
|
-
|
|
613
|
-
return (await sidecar.call('providers.auth_status', { profile_home: profileHome, provider: pid })).status;
|
|
614
|
-
}
|
|
615
|
-
catch {
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
655
|
+
return this.poolLookup(profileHome, `auth\0${pid}`, async (sidecar) => {
|
|
656
|
+
const { status } = await sidecar.call('providers.auth_status', { profile_home: profileHome, provider: pid });
|
|
657
|
+
return { logged_in: status.logged_in, key_source: str(status.key_source), error: str(status.error) };
|
|
658
|
+
});
|
|
618
659
|
}
|
|
619
660
|
/** `pooled`: the providers whose persisted credential pool holds an added account (TAL-638). */
|
|
620
|
-
async probeKey(profileHome, pid, config, envValues, pooled) {
|
|
661
|
+
async probeKey(profileHome, pid, config, envValues, pooled, scopedKeys) {
|
|
662
|
+
let authoritative = true;
|
|
621
663
|
let hasKey = this.providerHasKey(pid, config, envValues, profileHome);
|
|
622
664
|
let isOauth = OAUTH_PROVIDERS.has(pid);
|
|
623
665
|
let keySource = 'none';
|
|
624
666
|
let authError = null;
|
|
667
|
+
let removable = false;
|
|
625
668
|
if (isOauth) {
|
|
626
669
|
keySource = 'oauth';
|
|
627
|
-
const
|
|
670
|
+
const lookup = await this.authStatus(profileHome, pid);
|
|
671
|
+
authoritative = lookup.authoritative;
|
|
672
|
+
const status = lookup.value;
|
|
628
673
|
if (status?.logged_in) {
|
|
629
674
|
hasKey = true;
|
|
630
675
|
keySource = str(status.key_source) || 'oauth';
|
|
@@ -641,8 +686,10 @@ export class ProviderCatalog {
|
|
|
641
686
|
else if (hasKey) {
|
|
642
687
|
const envVar = providerEnvVar(pid);
|
|
643
688
|
if (envVar) {
|
|
644
|
-
if (valueCountsAsApiKey(pid, envValues[envVar]))
|
|
689
|
+
if (valueCountsAsApiKey(pid, envValues[envVar])) {
|
|
645
690
|
keySource = 'env_file';
|
|
691
|
+
removable = !pooled.has(pid) && !this.providerHasKey(pid, config, envValues, profileHome, envVar);
|
|
692
|
+
}
|
|
646
693
|
else if (valueCountsAsApiKey(pid, this.processEnv(envVar, profileHome)))
|
|
647
694
|
keySource = 'env_var';
|
|
648
695
|
else {
|
|
@@ -667,8 +714,16 @@ export class ProviderCatalog {
|
|
|
667
714
|
hasKey = true;
|
|
668
715
|
keySource = 'credential_pool';
|
|
669
716
|
}
|
|
717
|
+
else if (isBalanceProvider(pid)) {
|
|
718
|
+
if (scopedKeys[pid]?.present) {
|
|
719
|
+
hasKey = true;
|
|
720
|
+
keySource = 'env';
|
|
721
|
+
}
|
|
722
|
+
}
|
|
670
723
|
else if (!providerEnvVar(pid) && /^[a-z][a-z0-9_-]{0,63}$/.test(pid)) {
|
|
671
|
-
const
|
|
724
|
+
const lookup = await this.authStatus(profileHome, pid);
|
|
725
|
+
authoritative = lookup.authoritative;
|
|
726
|
+
const status = lookup.value;
|
|
672
727
|
if (status?.logged_in) {
|
|
673
728
|
hasKey = true;
|
|
674
729
|
const raw = str(status.key_source);
|
|
@@ -676,14 +731,21 @@ export class ProviderCatalog {
|
|
|
676
731
|
isOauth = true;
|
|
677
732
|
}
|
|
678
733
|
}
|
|
679
|
-
return { hasKey, keySource, authError, isOauth };
|
|
734
|
+
return { authoritative, hasKey, keySource, authError, isOauth, removable };
|
|
680
735
|
}
|
|
681
736
|
/** Live model ids from the Agent for one provider, cached per profile home. */
|
|
682
737
|
async liveModelIds(profileHome, pid, opts = {}) {
|
|
738
|
+
const inputSource = this.sourceFingerprint(profileHome, false);
|
|
739
|
+
if (!opts.source || opts.source !== this.sourceFingerprint(profileHome))
|
|
740
|
+
await this.poolProviders(profileHome);
|
|
683
741
|
const key = `${profileHome}\0${pid}`;
|
|
684
|
-
const source = this.sourceFingerprint(profileHome);
|
|
742
|
+
const source = inputSource === this.sourceFingerprint(profileHome, false) ? this.sourceFingerprint(profileHome) : inputSource;
|
|
685
743
|
const hit = this.liveIds.get(key);
|
|
686
|
-
if (
|
|
744
|
+
if (this.scopedKeyAuthority.get(profileHome) === false) {
|
|
745
|
+
this.liveFailed.add(`${key}\0${source}`);
|
|
746
|
+
return hit?.source === source ? hit.ids : [];
|
|
747
|
+
}
|
|
748
|
+
if (hit?.source === source && !opts.force && this.deps.now() - hit.at < hit.ttl)
|
|
687
749
|
return hit.ids;
|
|
688
750
|
// A lookup started under another source answers for the old endpoint or credential; this one starts its own.
|
|
689
751
|
const flightKey = `${key}\0${source}`;
|
|
@@ -694,51 +756,75 @@ export class ProviderCatalog {
|
|
|
694
756
|
// No sidecar is a failed lookup for this source: the ids it keeps showing never confirm a stale-model repair (TAL-542).
|
|
695
757
|
if (!sidecar) {
|
|
696
758
|
this.liveFailed.add(flightKey);
|
|
697
|
-
return hit?.ids
|
|
759
|
+
return hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.ids : [];
|
|
698
760
|
}
|
|
699
761
|
const run = sidecar.call('providers.model_ids', { profile_home: profileHome, provider: pid, ...(opts.force ? { force_refresh: true } : {}) }, { timeoutMs: 30_000 })
|
|
700
|
-
.then((r) => {
|
|
701
|
-
|
|
762
|
+
.then((r) => {
|
|
763
|
+
if (source !== this.sourceFingerprint(profileHome)) {
|
|
764
|
+
this.liveFailed.add(flightKey);
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
767
|
+
if (r.fallback && hit?.source === source && hit.confirmed) {
|
|
768
|
+
this.liveFailed.add(flightKey);
|
|
769
|
+
return hit.ids;
|
|
770
|
+
}
|
|
771
|
+
this.liveIds.set(key, { at: this.deps.now(), ids: r.model_ids, source, ttl: r.fallback ? FALLBACK_TTL_S : LIVE_TTL_S, confirmed: !r.fallback });
|
|
772
|
+
// TAL-799: the Agent's curated placeholder for an unreachable catalog is a failed lookup that still lists models.
|
|
773
|
+
if (r.fallback)
|
|
774
|
+
this.liveFailed.add(flightKey);
|
|
775
|
+
else
|
|
776
|
+
this.liveFailed.delete(flightKey);
|
|
777
|
+
return r.model_ids;
|
|
778
|
+
})
|
|
779
|
+
.catch((error) => { this.deps.log(`[catalog] live model ids for ${pid} failed: ${str(error.message)}`); this.liveFailed.add(flightKey); return hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.ids : []; })
|
|
702
780
|
.finally(() => { this.liveInflight.delete(flightKey); });
|
|
703
781
|
this.liveInflight.set(flightKey, run);
|
|
704
782
|
return run;
|
|
705
783
|
}
|
|
784
|
+
/** Whether the last live lookup for this provider under `source` failed or answered with the Agent's placeholder list. */
|
|
785
|
+
liveLookupFailed(profileHome, pid, source = this.sourceFingerprint(profileHome)) {
|
|
786
|
+
return this.liveFailed.has(`${profileHome}\0${pid}\0${source}`);
|
|
787
|
+
}
|
|
706
788
|
/**
|
|
707
789
|
* TAL-288: the model-provider plugins installed and enabled in this profile, as the sidecar reads them from the Agent.
|
|
708
790
|
* A failed lookup lists none, so every other provider stays usable; an id that cannot be a provider routing hint is dropped.
|
|
709
791
|
*/
|
|
710
792
|
async pluginProviders(profileHome) {
|
|
711
|
-
|
|
712
|
-
if (!sidecar)
|
|
713
|
-
return [];
|
|
714
|
-
try {
|
|
793
|
+
return this.poolLookup(profileHome, 'plugins', async (sidecar) => {
|
|
715
794
|
const { providers } = await sidecar.call('plugins.providers', { profile_home: profileHome }, { timeoutMs: PLUGIN_LIST_TIMEOUT_MS });
|
|
716
795
|
return providers.filter((p, i, all) => /^[a-z0-9][a-z0-9._-]{0,63}$/.test(p.name) && all.findIndex((q) => q.name === p.name) === i);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* TAL-798: the GGUFs staged for the Agent's managed llama.cpp provider, selectable before its server runs (a turn boots
|
|
800
|
+
* it). Null when none are staged or the listing failed, so every other provider stays usable.
|
|
801
|
+
*/
|
|
802
|
+
async localModels(profileHome) {
|
|
803
|
+
return this.poolLookup(profileHome, 'local-models', async (sidecar) => sidecar.call('providers.local_models', { profile_home: profileHome }, { timeoutMs: PLUGIN_LIST_TIMEOUT_MS }));
|
|
722
804
|
}
|
|
723
805
|
/** Python `get_providers`. */
|
|
724
806
|
async providers(profileHome) {
|
|
725
807
|
return structuredClone((await this.providerRows(profileHome)).payload);
|
|
726
808
|
}
|
|
727
809
|
/** The cached `providers` answer, shared with `quotas`; callers never mutate it. */
|
|
728
|
-
async providerRows(profileHome) {
|
|
810
|
+
async providerRows(profileHome, pool) {
|
|
729
811
|
const config = await this.deps.config.read(profileHome);
|
|
730
812
|
const envValues = loadEnvFile(join(profileHome, '.env'));
|
|
731
|
-
|
|
813
|
+
pool ??= await this.poolProviders(profileHome);
|
|
814
|
+
const source = this.sourceFingerprint(profileHome);
|
|
815
|
+
const cacheKey = source;
|
|
732
816
|
const hit = this.providersCache.get(profileHome);
|
|
733
|
-
if (hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
|
|
817
|
+
if (pool.authoritative && hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
|
|
734
818
|
return hit;
|
|
735
|
-
const pooledIds =
|
|
819
|
+
const pooledIds = pool.value?.providers ?? null;
|
|
820
|
+
const pluginLookup = await this.pluginProviders(profileHome);
|
|
821
|
+
let authoritative = pool.authoritative && pluginLookup.authoritative;
|
|
736
822
|
const pooled = new Set(pooledIds ?? []);
|
|
737
823
|
const active = activeProviderFromConfig(config);
|
|
738
824
|
const known = new Set([...Object.keys(PROVIDER_DISPLAY), ...Object.keys(PROVIDER_MODELS), ...OAUTH_PROVIDERS]);
|
|
739
825
|
// A plugin never stands in for a built-in provider, by id or alias (that would route to the built-in's billing); it
|
|
740
826
|
// does own a `providers.<id>` config entry.
|
|
741
|
-
const plugins = (
|
|
827
|
+
const plugins = (pluginLookup.value ?? []).filter((p) => !known.has(p.name) && !known.has(providerIdentity(p.name)));
|
|
742
828
|
const pluginIds = new Set(plugins.map((p) => p.name));
|
|
743
829
|
const providersCfg = dict(config.providers);
|
|
744
830
|
for (const key of Object.keys(providersCfg)) {
|
|
@@ -749,7 +835,8 @@ export class ProviderCatalog {
|
|
|
749
835
|
}
|
|
750
836
|
const rows = [];
|
|
751
837
|
for (const pid of [...known].sort()) {
|
|
752
|
-
const probe = await this.probeKey(profileHome, pid, config, envValues, pooled);
|
|
838
|
+
const probe = await this.probeKey(profileHome, pid, config, envValues, pooled, pool.value?.keys ?? {});
|
|
839
|
+
authoritative &&= probe.authoritative;
|
|
753
840
|
if (pid === 'openai' && !probe.hasKey && looksLikeCodexOauthToken(str(envValues[providerEnvVar('openai') ?? ''] ?? this.deps.env.OPENAI_API_KEY)))
|
|
754
841
|
continue;
|
|
755
842
|
const configured = probe.hasKey || selfHostedEndpoint(pid, config);
|
|
@@ -758,7 +845,7 @@ export class ProviderCatalog {
|
|
|
758
845
|
// Python: the card prefers the live catalog for every keyed provider (exactly like the picker), keeping the
|
|
759
846
|
// static list as the cold/failed-probe fallback; Nous renders a featured subset with the full count.
|
|
760
847
|
if (configured && (probe.isOauth || pid === 'lmstudio' || pid === 'nous' || !BESPOKE_CATALOG_PROVIDERS.has(pid))) {
|
|
761
|
-
const live = await this.liveModelIds(profileHome, pid);
|
|
848
|
+
const live = await this.liveModelIds(profileHome, pid, { source });
|
|
762
849
|
if (live.length) {
|
|
763
850
|
models = pid === 'nous' ? live.slice(0, 25).map((id) => ({ id: `@nous:${id}`, label: `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` })) : live.map((id) => ({ id, label: labelForModel(id, []) }));
|
|
764
851
|
modelsTotal = live.length;
|
|
@@ -785,6 +872,7 @@ export class ProviderCatalog {
|
|
|
785
872
|
is_self_hosted: SELF_HOSTED_PROVIDER_IDS.has(pid),
|
|
786
873
|
is_custom: false,
|
|
787
874
|
key_source: probe.keySource,
|
|
875
|
+
removable: probe.removable && !SELF_HOSTED_PROVIDER_IDS.has(pid),
|
|
788
876
|
base_url: baseUrl,
|
|
789
877
|
auth_error: probe.authError,
|
|
790
878
|
env_var: providerEnvVar(pid),
|
|
@@ -798,7 +886,7 @@ export class ProviderCatalog {
|
|
|
798
886
|
const live = ready ? await this.liveModelIds(profileHome, plugin.name) : [];
|
|
799
887
|
rows.push({
|
|
800
888
|
id: plugin.name, display_name: plugin.display_name || plugin.name, has_key: ready, configured: ready,
|
|
801
|
-
configurable: false, is_oauth: false, is_plugin_provider: true, is_self_hosted: false, is_custom: false, key_source: ready ? 'plugin' : 'none',
|
|
889
|
+
configurable: false, is_oauth: false, is_plugin_provider: true, is_self_hosted: false, is_custom: false, key_source: ready ? 'plugin' : 'none', removable: false,
|
|
802
890
|
base_url: null, auth_error: plugin.setup === 'ready' ? (live.length ? null : PLUGIN_NO_MODELS) : PLUGIN_SETUP_ERRORS[plugin.setup], env_var: null,
|
|
803
891
|
models: live.map((id) => ({ id, label: labelForModel(id, []) })), models_total: live.length,
|
|
804
892
|
});
|
|
@@ -812,32 +900,50 @@ export class ProviderCatalog {
|
|
|
812
900
|
// Python: an `api_key: ${VAR}` reference counts when the variable resolves.
|
|
813
901
|
const cpKey = str(cp.api_key).trim();
|
|
814
902
|
const envRef = /^\$\{([^}]+)\}$/.exec(cpKey)?.[1] ?? '';
|
|
815
|
-
const
|
|
903
|
+
const yamlKey = !envRef && valueCountsAsApiKey(slug, cp.api_key);
|
|
904
|
+
const hasKey = envRef ? Boolean((this.processEnv(envRef, profileHome) ?? '').trim()) : yamlKey || Boolean(str(cp.key_env).trim() && this.processEnv(str(cp.key_env).trim(), profileHome));
|
|
905
|
+
// TAL-603: a `${VAR}` reference or `key_env` credential comes from the environment.
|
|
906
|
+
const keySource = yamlKey ? 'config_yaml' : hasKey ? 'env_var' : cpKey ? 'config_yaml' : 'none';
|
|
816
907
|
rows.push({
|
|
817
908
|
id: slug, display_name: name, has_key: hasKey, configured: hasKey,
|
|
818
|
-
configurable: false, is_oauth: false, is_plugin_provider: false, is_self_hosted: false, is_custom: true, key_source:
|
|
909
|
+
configurable: false, is_oauth: false, is_plugin_provider: false, is_self_hosted: false, is_custom: true, key_source: keySource, removable: false,
|
|
819
910
|
base_url: str(cp.base_url).trim() || null, auth_error: null, env_var: null, models: ids.map((id) => ({ id, label: labelForModel(id, []) })), models_total: ids.length,
|
|
820
911
|
});
|
|
821
912
|
}
|
|
822
913
|
// Python `_provider_sort_key`: active first, then `custom:*`, then keyed providers, then the rest (alphabetical within).
|
|
823
914
|
const rank = (p) => (str(p.id) === active ? 0 : str(p.id).startsWith('custom:') ? 1 : p.has_key ? 2 : 3);
|
|
824
915
|
rows.sort((a, b) => rank(a) - rank(b) || (str(a.id) < str(b.id) ? -1 : str(a.id) > str(b.id) ? 1 : 0));
|
|
825
|
-
for (const row of rows)
|
|
916
|
+
for (const row of rows) {
|
|
826
917
|
row.models = stampModelEntries(row.models, str(row.id));
|
|
827
|
-
|
|
828
|
-
|
|
918
|
+
// TAL-603: the server decides the Active badge, the key badge and the count; a featured list never outnumbers its total.
|
|
919
|
+
row.is_active = str(row.id) === active;
|
|
920
|
+
row.key_source_kind = keySourceKind(row);
|
|
921
|
+
row.models_total = Math.max(Number(row.models_total), row.models.length);
|
|
922
|
+
}
|
|
923
|
+
authoritative &&= source === this.sourceFingerprint(profileHome);
|
|
924
|
+
const availability = this.availability(source, authoritative, Boolean(pool.value));
|
|
925
|
+
const entry = { at: this.deps.now(), key: cacheKey, payload: { providers: rows, active_provider: active, availability }, pooled: pooledIds };
|
|
926
|
+
if (authoritative)
|
|
927
|
+
this.providersCache.set(profileHome, entry);
|
|
829
928
|
return entry;
|
|
830
929
|
}
|
|
831
930
|
/** Python `get_available_models` (static catalog + live ids for keyed providers). */
|
|
832
|
-
async models(profileHome) {
|
|
931
|
+
async models(profileHome, attempt = 0) {
|
|
932
|
+
const pool = await this.poolProviders(profileHome);
|
|
833
933
|
const source = this.sourceFingerprint(profileHome);
|
|
934
|
+
const providerState = (await this.providerRows(profileHome, pool)).payload;
|
|
834
935
|
const unconfirmed = new Set();
|
|
835
936
|
const config = await this.deps.config.read(profileHome);
|
|
836
937
|
const envValues = loadEnvFile(join(profileHome, '.env'));
|
|
837
|
-
const active = activeProviderFromConfig(config);
|
|
838
938
|
const model = modelSection(config);
|
|
839
939
|
const defaultModel = effectiveDefaultModel(config, this.deps.env);
|
|
840
940
|
const cfgBaseUrl = str(model.base_url).trim();
|
|
941
|
+
const localLookup = await this.localModels(profileHome);
|
|
942
|
+
const local = localLookup.value?.models.length ? localLookup.value : null;
|
|
943
|
+
const configured = activeProviderFromConfig(config);
|
|
944
|
+
// A `custom` main model at the running local server's endpoint is that server: its Local group owns the selection.
|
|
945
|
+
const atLocalServer = configured === 'custom' && Boolean(cfgBaseUrl) && cfgBaseUrl.replace(/\/+$/, '') === str(local?.current_endpoint).replace(/\/+$/, '');
|
|
946
|
+
const active = local && atLocalServer ? local.provider_id : configured;
|
|
841
947
|
const detected = new Set();
|
|
842
948
|
const configuredIds = new Map();
|
|
843
949
|
const append = (pid, id) => {
|
|
@@ -851,6 +957,8 @@ export class ProviderCatalog {
|
|
|
851
957
|
};
|
|
852
958
|
if (active)
|
|
853
959
|
detected.add(active);
|
|
960
|
+
if (local)
|
|
961
|
+
detected.add(local.provider_id);
|
|
854
962
|
const providersCfg = dict(config.providers);
|
|
855
963
|
const rawKeyFor = new Map();
|
|
856
964
|
for (const [key, value] of Object.entries(providersCfg)) {
|
|
@@ -878,8 +986,8 @@ export class ProviderCatalog {
|
|
|
878
986
|
// ready plugin providers (TAL-288), under their own name, and providers keyed only through the credential pool (TAL-638).
|
|
879
987
|
const signedIn = new Set();
|
|
880
988
|
const pluginNames = new Map();
|
|
881
|
-
for (const row of
|
|
882
|
-
if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true && row.key_source !== 'credential_pool'))
|
|
989
|
+
for (const row of providerState.providers) {
|
|
990
|
+
if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true && row.key_source !== 'credential_pool' && row.key_source !== 'env'))
|
|
883
991
|
continue;
|
|
884
992
|
signedIn.add(str(row.id));
|
|
885
993
|
if (row.is_plugin_provider === true)
|
|
@@ -917,8 +1025,15 @@ export class ProviderCatalog {
|
|
|
917
1025
|
const named = [...namedCustom.entries()].find(([, v]) => customProviderEntries(config).some((e) => customProviderSlug(e.name) === customProviderSlug(v.name) && str(e.base_url).trim().replace(/\/+$/, '') === cfgBaseUrl.replace(/\/+$/, '')));
|
|
918
1026
|
detected.add(named?.[0] ?? active ?? 'custom');
|
|
919
1027
|
}
|
|
1028
|
+
// TAL-911: Agent pickers drop `model_catalog.excluded_providers` under any of their aliases, the active provider included.
|
|
1029
|
+
const excludedCfg = dict(config.model_catalog).excluded_providers;
|
|
1030
|
+
const excluded = new Set(Array.isArray(excludedCfg) ? excludedCfg.map(providerIdentity).filter(Boolean) : []);
|
|
1031
|
+
const isExcluded = (pid) => excluded.has(providerIdentity(pid));
|
|
1032
|
+
const pickerDefault = isExcluded(active) || isExcluded(parseProviderQualifiedModel(defaultModel)?.[1]) ? '' : defaultModel;
|
|
920
1033
|
const groups = [];
|
|
921
1034
|
for (const pid of [...detected].map((p) => (p.startsWith('custom') || pluginNames.has(p) ? p : canonicaliseProviderId(p) || p)).filter((v, i, a) => v && a.indexOf(v) === i).sort()) {
|
|
1035
|
+
if (isExcluded(pid))
|
|
1036
|
+
continue;
|
|
922
1037
|
if (pid.startsWith('custom:')) {
|
|
923
1038
|
const g = namedCustom.get(pid);
|
|
924
1039
|
const models = [...(g?.models ?? [])];
|
|
@@ -926,6 +1041,11 @@ export class ProviderCatalog {
|
|
|
926
1041
|
groups.push({ provider: g?.name ?? pid.replace('custom:', ''), provider_id: pid, models: applyProviderPrefix(models, pid, active) });
|
|
927
1042
|
continue;
|
|
928
1043
|
}
|
|
1044
|
+
if (pid === local?.provider_id) {
|
|
1045
|
+
// A GGUF's id is its file name, quant tag included (`Q4_K_M`); prettifying would mangle it.
|
|
1046
|
+
groups.push({ provider: 'Local', provider_id: pid, models: applyProviderPrefix(local.models.map((id) => ({ id, label: id })), pid, active) });
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
929
1049
|
if (pid === 'custom') {
|
|
930
1050
|
const models = [...customModels];
|
|
931
1051
|
for (const id of configuredIds.get(pid) ?? [])
|
|
@@ -937,20 +1057,25 @@ export class ProviderCatalog {
|
|
|
937
1057
|
}
|
|
938
1058
|
const providerCfg = dict(providersCfg[rawKeyFor.get(pid) ?? pid]);
|
|
939
1059
|
let raw = [];
|
|
1060
|
+
let allowFallback = false;
|
|
940
1061
|
if ('models' in providerCfg && providerCfg.models_discovered !== true)
|
|
941
1062
|
raw = configuredModelOptions(providerCfg.models);
|
|
942
|
-
if (!raw.length
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1063
|
+
if (!raw.length) {
|
|
1064
|
+
// TAL-799: a provider without credentials lists the Agent's curated catalog too; the static tables only cover a missing sidecar.
|
|
1065
|
+
const keyed = this.providerHasKey(pid, config, envValues, profileHome) || selfHostedEndpoint(pid, config) || signedIn.has(pid);
|
|
1066
|
+
const live = await this.liveModelIds(profileHome, pid, { source });
|
|
1067
|
+
const failed = this.liveLookupFailed(profileHome, pid, source);
|
|
1068
|
+
// TAL-542: a failed lookup, or a curated list no account confirmed, leaves this group unconfirmed, so the stale-model repair never trusts it.
|
|
1069
|
+
allowFallback = failed || !keyed;
|
|
1070
|
+
if (allowFallback)
|
|
946
1071
|
unconfirmed.add(pid);
|
|
947
1072
|
if (live.length)
|
|
948
1073
|
raw = live.map((id) => ({ id, label: pid === 'nous' ? `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` : labelForModel(id, []) }));
|
|
949
1074
|
// Python (#1567): an authenticated Nous account with an empty live catalog shows no group; only a failed lookup falls back to the curated list.
|
|
950
|
-
else if (pid === 'nous' && !
|
|
1075
|
+
else if (keyed && pid === 'nous' && !failed)
|
|
951
1076
|
continue;
|
|
952
1077
|
}
|
|
953
|
-
if (!raw.length)
|
|
1078
|
+
if (!raw.length && allowFallback)
|
|
954
1079
|
raw = pid === 'openrouter' ? FALLBACK_MODELS.map((m) => ({ id: m.id, label: m.label })) : [...(PROVIDER_MODELS[pid] ?? [])];
|
|
955
1080
|
for (const id of configuredIds.get(pid) ?? [])
|
|
956
1081
|
if (!raw.some((m) => m.id === id))
|
|
@@ -958,7 +1083,7 @@ export class ProviderCatalog {
|
|
|
958
1083
|
if (raw.length)
|
|
959
1084
|
groups.push({ provider: pluginNames.get(pid) ?? displayName(pid), provider_id: pid, models: applyProviderPrefix(raw, pid, active) });
|
|
960
1085
|
}
|
|
961
|
-
if (
|
|
1086
|
+
if (pickerDefault) {
|
|
962
1087
|
const all = new Set(groups.flatMap((g) => g.models.map((m) => m.id)));
|
|
963
1088
|
if (!all.has(defaultModel) && !all.has(`@${active ?? ''}:${defaultModel}`)) {
|
|
964
1089
|
const label = labelForModel(defaultModel, groups);
|
|
@@ -991,11 +1116,6 @@ export class ProviderCatalog {
|
|
|
991
1116
|
}
|
|
992
1117
|
const rank = (g) => (g.provider_id === active ? 0 : g.provider_id.startsWith('custom:') ? 1 : withKeys.has(g.provider_id) ? 2 : 3);
|
|
993
1118
|
kept.sort((a, b) => rank(a) - rank(b) || (a.provider_id < b.provider_id ? -1 : a.provider_id > b.provider_id ? 1 : 0));
|
|
994
|
-
for (const g of kept)
|
|
995
|
-
if (isOpenAiFamilyProvider(g.provider_id))
|
|
996
|
-
for (const m of g.models)
|
|
997
|
-
if (/^(@[^:]+:)?(openai\/)?(gpt-5|o[0-9])/.test(m.id))
|
|
998
|
-
m.supports_fast_tier = true;
|
|
999
1119
|
const aliases = {};
|
|
1000
1120
|
const rawAliases = model.aliases;
|
|
1001
1121
|
if (isDict(rawAliases))
|
|
@@ -1003,19 +1123,118 @@ export class ProviderCatalog {
|
|
|
1003
1123
|
if (k && v)
|
|
1004
1124
|
aliases[k.trim()] = str(v).trim();
|
|
1005
1125
|
const [defaultBare, defaultProvider] = parseProviderQualifiedModel(defaultModel) ?? [defaultModel, active];
|
|
1006
|
-
|
|
1007
|
-
|
|
1126
|
+
if (source !== this.sourceFingerprint(profileHome)) {
|
|
1127
|
+
if (attempt === 0)
|
|
1128
|
+
return this.models(profileHome, 1);
|
|
1129
|
+
throw new Error('Provider configuration changed during catalog lookup. Try again.');
|
|
1130
|
+
}
|
|
1131
|
+
if (!providerState.availability.authoritative || !localLookup.authoritative)
|
|
1132
|
+
for (const g of kept)
|
|
1133
|
+
unconfirmed.add(g.provider_id);
|
|
1134
|
+
const previous = this.currentModels(profileHome);
|
|
1135
|
+
const availability = this.availability(source, providerState.availability.authoritative && localLookup.authoritative && !unconfirmed.size && source === this.sourceFingerprint(profileHome), previous?.availability?.status === 'available' || previous?.availability?.status === 'stale');
|
|
1136
|
+
for (const g of kept)
|
|
1137
|
+
g.availability = this.availability(source, !unconfirmed.has(g.provider_id) && providerState.availability.authoritative, Boolean(previous?.groups.some((old) => old.provider_id === g.provider_id && old.availability?.status !== 'unavailable')));
|
|
1138
|
+
const defaults = { availability, active_provider: active, default_model: defaultModel, default_provider_id: defaultProvider, default_bare_id: defaultBare };
|
|
1139
|
+
if (!kept.length && pickerDefault) {
|
|
1008
1140
|
const providerId = active ?? 'default';
|
|
1009
|
-
return this.remember(profileHome, { source, unconfirmed }, { ...defaults, groups: [{ provider: 'Default', provider_id: providerId, models: stampModelEntries([{ id: defaultModel, label: labelForModel(defaultModel, []) }], providerId) }], aliases: {}, configured_model_badges: {} });
|
|
1141
|
+
return this.remember(profileHome, { source, unconfirmed, attempt }, { ...defaults, groups: [{ provider: 'Default', provider_id: providerId, models: stampModelEntries([{ id: defaultModel, label: labelForModel(defaultModel, []) }], providerId) }], aliases: {}, configured_model_badges: {} });
|
|
1010
1142
|
}
|
|
1011
1143
|
const stamped = kept.map((g) => ({ ...g, models: stampModelEntries(g.models, g.provider_id), ...(g.extra_models ? { extra_models: stampModelEntries(g.extra_models, g.provider_id) } : {}) }));
|
|
1012
|
-
return this.remember(profileHome, { source, unconfirmed }, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
|
|
1144
|
+
return this.remember(profileHome, { source, unconfirmed, attempt }, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* TAL-807: the picker catalog (`/api/models`) with each group's live pool rate limit, read per request so an expired one
|
|
1148
|
+
* is gone, and its quota level. The cached catalog holds neither; a failed read leaves them out.
|
|
1149
|
+
*/
|
|
1150
|
+
async pickerModels(profileHome, profile) {
|
|
1151
|
+
const catalog = await this.models(profileHome);
|
|
1152
|
+
const entries = (g) => [...g.models, ...(g.extra_models ?? [])];
|
|
1153
|
+
let limits = {};
|
|
1154
|
+
try {
|
|
1155
|
+
const sidecar = this.deps.sidecar();
|
|
1156
|
+
const providers = catalog.groups.map((g) => ({ slug: g.provider_id, models: entries(g).map((e) => e.bare_id ?? e.id) }));
|
|
1157
|
+
if (sidecar)
|
|
1158
|
+
limits = (await sidecar.call('usage.limits', { profile_home: profileHome, providers })).limits;
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
this.deps.log(`[catalog] pool limits failed: ${str(error.message)}`);
|
|
1162
|
+
}
|
|
1163
|
+
for (const g of catalog.groups) {
|
|
1164
|
+
const limit = limits[g.provider_id];
|
|
1165
|
+
if (limit?.scope === 'account')
|
|
1166
|
+
g.limit = limit;
|
|
1167
|
+
if (limit?.scope === 'models') {
|
|
1168
|
+
// The sidecar answers by bare id; the picker names entries by id.
|
|
1169
|
+
const cooled = {};
|
|
1170
|
+
for (const e of entries(g)) {
|
|
1171
|
+
const resetsAt = limit.models[e.bare_id ?? e.id];
|
|
1172
|
+
if (resetsAt) {
|
|
1173
|
+
e.limit = { scope: 'model', resets_at: resetsAt };
|
|
1174
|
+
cooled[e.id] = resetsAt;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (Object.keys(cooled).length)
|
|
1178
|
+
g.limit = { scope: 'models', models: cooled };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const thresholds = (await this.deps.quotaThresholds?.(profile)) ?? QUOTA_THRESHOLD_DEFAULTS;
|
|
1182
|
+
// An account limit already says the login is out; the quota level would only repeat it.
|
|
1183
|
+
const levels = await Promise.all(catalog.groups.map((g) => (g.limit?.scope === 'account' ? Promise.resolve(null) : this.pickerQuotaLevel(profileHome, g.provider_id, thresholds))));
|
|
1184
|
+
catalog.groups.forEach((g, i) => { const level = levels[i]; if (level)
|
|
1185
|
+
g.quota_level = level; });
|
|
1186
|
+
return catalog;
|
|
1187
|
+
}
|
|
1188
|
+
/** TAL-807: account-usage reads the picker started, by `accountUsageCache` key. */
|
|
1189
|
+
pickerUsageReads = new Map();
|
|
1190
|
+
/**
|
|
1191
|
+
* TAL-807: the TAL-411 `remaining` level of the tightest account-scope window in the provider's cached account-usage
|
|
1192
|
+
* read; a model-family window (Opus or Sonnet weekly) never counts. A read past the cache TTL starts a new one, which
|
|
1193
|
+
* the picker waits for only when it has no read at all, and then for at most `PICKER_USAGE_WAIT_MS`; one past
|
|
1194
|
+
* `PICKER_USAGE_MAX_AGE_S` shows nothing.
|
|
1195
|
+
*/
|
|
1196
|
+
async pickerQuotaLevel(profileHome, pid, t) {
|
|
1197
|
+
if (!ACCOUNT_USAGE_PROVIDERS.has(pid))
|
|
1198
|
+
return null;
|
|
1199
|
+
const key = `${profileHome}\0${pid}\0`;
|
|
1200
|
+
let cached = this.accountUsageCache.get(key);
|
|
1201
|
+
if (cached?.source !== this.sourceFingerprint(profileHome))
|
|
1202
|
+
cached = undefined;
|
|
1203
|
+
if (!cached || this.deps.now() - cached.at > ACCOUNT_USAGE_CACHE_TTL_S) {
|
|
1204
|
+
let read = this.pickerUsageReads.get(key);
|
|
1205
|
+
if (!read) {
|
|
1206
|
+
read = this.quota(profileHome, pid).then(() => undefined, () => undefined).finally(() => this.pickerUsageReads.delete(key));
|
|
1207
|
+
this.pickerUsageReads.set(key, read);
|
|
1208
|
+
}
|
|
1209
|
+
if (!cached) {
|
|
1210
|
+
let timer;
|
|
1211
|
+
await Promise.race([read, new Promise((resolve) => { timer = setTimeout(resolve, PICKER_USAGE_WAIT_MS); })]);
|
|
1212
|
+
clearTimeout(timer);
|
|
1213
|
+
cached = this.accountUsageCache.get(key);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const limits = cached?.limits;
|
|
1217
|
+
if (cached?.source !== this.sourceFingerprint(profileHome) || !limits?.available || this.deps.now() - cached.at > PICKER_USAGE_MAX_AGE_S)
|
|
1218
|
+
return null;
|
|
1219
|
+
const raw = (Array.isArray(limits.windows) ? limits.windows : []).filter((w) => isDict(w) && w.scope !== 'model');
|
|
1220
|
+
const tightest = normalizeQuotaWindows(raw, this.deps.now()).windows
|
|
1221
|
+
.reduce((a, w) => (w.remaining_percent !== null && w.remaining_percent < (a?.remaining_percent ?? Infinity) ? w : a), undefined);
|
|
1222
|
+
return tightest ? quotaLevel(tightest, limits.stale ? 'stale' : 'available', 'remaining', t) : null;
|
|
1013
1223
|
}
|
|
1014
1224
|
/** Stamps the default's entry id and keeps the catalog for `modelOptionFor`. */
|
|
1015
|
-
remember(profileHome, meta, catalog) {
|
|
1225
|
+
async remember(profileHome, meta, catalog) {
|
|
1226
|
+
if (catalog.availability)
|
|
1227
|
+
for (const group of catalog.groups)
|
|
1228
|
+
group.availability ??= catalog.availability;
|
|
1229
|
+
await this.stampSpeedTiers(profileHome, catalog.groups.flatMap((g) => [...g.models, ...(g.extra_models ?? [])]));
|
|
1230
|
+
if (meta.source !== this.sourceFingerprint(profileHome)) {
|
|
1231
|
+
if (meta.attempt === 0)
|
|
1232
|
+
return this.models(profileHome, 1);
|
|
1233
|
+
throw new Error('Provider configuration changed during catalog lookup. Try again.');
|
|
1234
|
+
}
|
|
1016
1235
|
catalog.default_option_id = catalogOptionId(catalog, catalog.default_bare_id ?? null, catalog.default_provider_id ?? null);
|
|
1017
1236
|
this.lastModels.set(profileHome, catalog);
|
|
1018
|
-
this.lastModelsMeta.set(profileHome, {
|
|
1237
|
+
this.lastModelsMeta.set(profileHome, { source: meta.source, unconfirmed: meta.unconfirmed, at: this.deps.now() });
|
|
1019
1238
|
return structuredClone(catalog);
|
|
1020
1239
|
}
|
|
1021
1240
|
/** Python `_configured_model_badges_from_static_catalog`: which picker rows are the main/fallback selections. */
|
|
@@ -1047,14 +1266,17 @@ export class ProviderCatalog {
|
|
|
1047
1266
|
return badges;
|
|
1048
1267
|
}
|
|
1049
1268
|
/** Python `_handle_live_models`. */
|
|
1050
|
-
async liveModels(profileHome, providerRaw) {
|
|
1269
|
+
async liveModels(profileHome, providerRaw, attempt = 0) {
|
|
1270
|
+
const fingerprint = this.sourceFingerprint(profileHome);
|
|
1051
1271
|
const config = await this.deps.config.read(profileHome);
|
|
1052
1272
|
let provider = providerRaw.trim().toLowerCase() || str(modelSection(config).provider).trim().toLowerCase();
|
|
1053
1273
|
if (!provider)
|
|
1054
1274
|
return { error: 'no_provider', models: [] };
|
|
1055
1275
|
provider = resolveProviderAlias(provider);
|
|
1056
1276
|
let ids = await this.liveModelIds(profileHome, provider);
|
|
1057
|
-
|
|
1277
|
+
if (fingerprint !== this.sourceFingerprint(profileHome))
|
|
1278
|
+
return attempt === 0 ? this.liveModels(profileHome, providerRaw, 1) : { availability: this.availability(this.sourceFingerprint(profileHome), false, false), provider, source: 'unavailable', models: [], count: 0 };
|
|
1279
|
+
let source = this.liveLookupFailed(profileHome, provider, fingerprint) ? 'fallback' : 'live';
|
|
1058
1280
|
if (!ids.length) {
|
|
1059
1281
|
const fromConfig = [];
|
|
1060
1282
|
if (provider === 'custom' || provider.startsWith('custom:')) {
|
|
@@ -1064,16 +1286,33 @@ export class ProviderCatalog {
|
|
|
1064
1286
|
fromConfig.push(...[str(cp.model).trim(), ...configuredModelIds(cp.models)].filter(Boolean));
|
|
1065
1287
|
}
|
|
1066
1288
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1289
|
+
if (fromConfig.length) {
|
|
1290
|
+
ids = [...new Set(fromConfig)];
|
|
1291
|
+
source = 'config';
|
|
1292
|
+
}
|
|
1293
|
+
else if (source === 'fallback') {
|
|
1294
|
+
ids = (PROVIDER_MODELS[provider] ?? []).map((m) => m.id);
|
|
1295
|
+
source = 'static';
|
|
1296
|
+
}
|
|
1069
1297
|
}
|
|
1070
|
-
// Python: the dropdown-enrichment surface keeps the picker's visibility budget (15 rows past 25) and annotates
|
|
1071
|
-
// OpenAI-family rows with `supports_fast_tier`; the answer carries `count`.
|
|
1072
1298
|
if (provider !== 'nous' && ids.length > MODEL_PICKER_OVERFLOW_THRESHOLD)
|
|
1073
1299
|
ids = ids.slice(0, MODEL_PICKER_VISIBLE_TARGET);
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1300
|
+
const models = stampModelEntries(ids.filter(Boolean).map((id) => ({ id, label: labelForModel(id, []) })), provider);
|
|
1301
|
+
await this.stampSpeedTiers(profileHome, models);
|
|
1302
|
+
if (fingerprint !== this.sourceFingerprint(profileHome))
|
|
1303
|
+
return attempt === 0 ? this.liveModels(profileHome, providerRaw, 1) : { availability: this.availability(this.sourceFingerprint(profileHome), false, false), provider, source: 'unavailable', models: [], count: 0 };
|
|
1304
|
+
return { availability: this.availability(fingerprint, source === 'live' || source === 'config', source === 'fallback' && models.length > 0), provider, source, models, count: models.length };
|
|
1305
|
+
}
|
|
1306
|
+
async stampSpeedTiers(profileHome, entries) {
|
|
1307
|
+
if (!entries.length)
|
|
1308
|
+
return;
|
|
1309
|
+
const config = await this.deps.config.read(profileHome);
|
|
1310
|
+
// An unavailable Agent offers no paid tier; writes still require a successful lookup.
|
|
1311
|
+
const tiers = await this.deps.config.speedTiers(profileHome, config, entries.map((e) => ({ model: e.bare_id ?? e.id, provider: e.provider_id ?? '' }))).catch(() => []);
|
|
1312
|
+
entries.forEach((entry, i) => {
|
|
1313
|
+
entry.supported_speed_tiers = tiers[i] ?? [];
|
|
1314
|
+
entry.supports_fast_tier = entry.supported_speed_tiers.some((t) => t.service_tier === 'priority');
|
|
1315
|
+
});
|
|
1077
1316
|
}
|
|
1078
1317
|
/**
|
|
1079
1318
|
* Python `_resolve_model_context_length` inputs: the effective base URL, key, and any configured window for this
|
|
@@ -1136,27 +1375,35 @@ export class ProviderCatalog {
|
|
|
1136
1375
|
if (envVar) {
|
|
1137
1376
|
for (const name of [envVar, ...(PROVIDER_ENV_VAR_ALIASES[pid] ?? [])]) {
|
|
1138
1377
|
const v = str(envValues[name] ?? this.processEnv(name, profileHome)).trim();
|
|
1378
|
+
// A reference owns this slot; only the Agent can resolve it before lower-priority keys.
|
|
1139
1379
|
if (v)
|
|
1140
|
-
return v;
|
|
1380
|
+
return v.startsWith('op://') ? null : v;
|
|
1141
1381
|
}
|
|
1142
1382
|
}
|
|
1143
1383
|
// The same config sources `providerHasKey` counts, so a provider shown as keyed resolves its key.
|
|
1144
1384
|
const model = modelSection(config);
|
|
1145
1385
|
if (canonicaliseProviderId(model.provider) === canonicaliseProviderId(pid) && valueCountsAsApiKey(pid, model.api_key))
|
|
1146
|
-
return str(model.api_key).trim();
|
|
1386
|
+
return str(model.api_key).trim().startsWith('op://') ? null : str(model.api_key).trim();
|
|
1147
1387
|
for (const [key, value] of Object.entries(dict(config.providers))) {
|
|
1148
1388
|
if (providerIdentity(key) === providerIdentity(pid) && isDict(value) && valueCountsAsApiKey(pid, value.api_key))
|
|
1149
|
-
return str(value.api_key).trim();
|
|
1389
|
+
return str(value.api_key).trim().startsWith('op://') ? null : str(value.api_key).trim();
|
|
1150
1390
|
}
|
|
1151
1391
|
return null;
|
|
1152
1392
|
}
|
|
1393
|
+
/** A raw key or the names the Agent resolves inside the profile's secret scope. */
|
|
1394
|
+
usageKey(pid, apiKey) {
|
|
1395
|
+
const envVar = providerEnvVar(pid);
|
|
1396
|
+
return apiKey ? { api_key: apiKey } : { env_vars: envVar ? [envVar, ...(PROVIDER_ENV_VAR_ALIASES[pid] ?? [])] : [] };
|
|
1397
|
+
}
|
|
1153
1398
|
/** Account-usage snapshots by profile home, provider, and pool account. */
|
|
1154
1399
|
accountUsageCache = new Map();
|
|
1400
|
+
quotaReadSequence = 0;
|
|
1155
1401
|
/**
|
|
1156
1402
|
* Python `get_provider_quota`. `credential` scopes the read to one credential-pool account (TAL-548): a benched account
|
|
1157
1403
|
* answers with its local pool state, and every other read uses that account's key.
|
|
1158
1404
|
*/
|
|
1159
1405
|
async quota(profileHome, providerRaw, opts = {}) {
|
|
1406
|
+
const sequence = opts.sequence ?? ++this.quotaReadSequence;
|
|
1160
1407
|
const at = opts.at ?? this.deps.now();
|
|
1161
1408
|
const computed_at = isoAt(at);
|
|
1162
1409
|
const config = await this.deps.config.read(profileHome);
|
|
@@ -1174,7 +1421,9 @@ export class ProviderCatalog {
|
|
|
1174
1421
|
let limits = null;
|
|
1175
1422
|
// Python `_ACCOUNT_USAGE_CACHE_TTL_SECONDS`: a snapshot answers repeat polls for 45 s unless `refresh` is set.
|
|
1176
1423
|
const cacheKey = `${profileHome}\0${provider}\0${credential?.credential_id ?? ''}`;
|
|
1177
|
-
const
|
|
1424
|
+
const hit = this.accountUsageCache.get(cacheKey);
|
|
1425
|
+
const source = this.sourceFingerprint(profileHome);
|
|
1426
|
+
const cached = hit?.source === source ? hit : undefined;
|
|
1178
1427
|
if (cached && !opts.refresh && this.deps.now() - cached.at <= ACCOUNT_USAGE_CACHE_TTL_S)
|
|
1179
1428
|
limits = cached.limits;
|
|
1180
1429
|
else if (sidecar) {
|
|
@@ -1187,7 +1436,12 @@ export class ProviderCatalog {
|
|
|
1187
1436
|
catch (error) {
|
|
1188
1437
|
limits = { available: false, unavailable_reason: str(error.message), windows: [], details: [] };
|
|
1189
1438
|
}
|
|
1190
|
-
|
|
1439
|
+
if (source === this.sourceFingerprint(profileHome)) {
|
|
1440
|
+
if ((this.accountUsageCache.get(cacheKey)?.sequence ?? -1) <= sequence)
|
|
1441
|
+
this.accountUsageCache.set(cacheKey, { at: this.deps.now(), limits, source, sequence });
|
|
1442
|
+
}
|
|
1443
|
+
else
|
|
1444
|
+
limits = null;
|
|
1191
1445
|
}
|
|
1192
1446
|
// The cache keeps the Agent's raw windows; pace is recomputed as of every response.
|
|
1193
1447
|
if (limits)
|
|
@@ -1197,11 +1451,11 @@ export class ProviderCatalog {
|
|
|
1197
1451
|
const reason = str(limits?.unavailable_reason).trim();
|
|
1198
1452
|
return answer(false, 'unavailable', reason ? `${name} account limits are unavailable. ${reason}` : `${name} account limits are unavailable. Confirm provider authentication and try again.`, { account_limits: limits });
|
|
1199
1453
|
}
|
|
1200
|
-
if (provider
|
|
1454
|
+
if (isBalanceProvider(provider)) {
|
|
1201
1455
|
const apiKey = this.apiKeyFor(provider, profileHome, config);
|
|
1202
|
-
const key = credential ? { credential_id: credential.credential_id } :
|
|
1203
|
-
const read =
|
|
1204
|
-
const status =
|
|
1456
|
+
const key = credential ? { credential_id: credential.credential_id } : this.usageKey(provider, apiKey);
|
|
1457
|
+
const read = await this.balance(profileHome, provider, key);
|
|
1458
|
+
const status = read?.status === 'no_key' ? 'no_key' : read?.status === 'ok' ? 'ok' : read?.status === 'http_error' ? read.http_status : null;
|
|
1205
1459
|
if (provider === 'openrouter') {
|
|
1206
1460
|
if (status === 'no_key')
|
|
1207
1461
|
return answer(false, 'no_key', 'OpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.');
|
|
@@ -1235,7 +1489,7 @@ export class ProviderCatalog {
|
|
|
1235
1489
|
return answer(false, 'unavailable', status === 403 ? 'OpenCode Go account limits require an active subscription.' : 'OpenCode Go account limits are temporarily unavailable.', { account_limits: null });
|
|
1236
1490
|
}
|
|
1237
1491
|
// Python `_local_pool_snapshot`: any other provider with a credential pool shows its accounts' local state.
|
|
1238
|
-
const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)) ?? [];
|
|
1492
|
+
const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)).value?.entries ?? [];
|
|
1239
1493
|
if (entries.length) {
|
|
1240
1494
|
const limits = credential ? poolEntryLimits(credential, at) : poolLimits(entries, at);
|
|
1241
1495
|
return limits.available ? answer(true, 'available', `${name} credential pool status loaded.`, { label: 'Credential pool', account_limits: limits }) : answer(false, 'unavailable', `${name} credential pool: all credentials are unavailable.`, { account_limits: limits });
|
|
@@ -1247,7 +1501,7 @@ export class ProviderCatalog {
|
|
|
1247
1501
|
* `apiKey`, the provider's configured key. Null when the pool is unknown (see `poolLookup`).
|
|
1248
1502
|
*/
|
|
1249
1503
|
poolEntries(profileHome, pid, apiKey) {
|
|
1250
|
-
return this.poolLookup(profileHome, `pool\0${pid}`,
|
|
1504
|
+
return this.poolLookup(profileHome, `pool\0${pid}`, (sidecar) => sidecar.call('usage.pool', { profile_home: profileHome, provider: pid, ...this.usageKey(pid, apiKey) }));
|
|
1251
1505
|
}
|
|
1252
1506
|
/** The last answer of each credential-pool lookup, by profile home and lookup. */
|
|
1253
1507
|
lastPoolAnswers = new Map();
|
|
@@ -1256,23 +1510,48 @@ export class ProviderCatalog {
|
|
|
1256
1510
|
* lookup, else null: an unknown pool, never an empty one, so no pool account is ever reported as removed.
|
|
1257
1511
|
*/
|
|
1258
1512
|
async poolLookup(profileHome, lookup, call) {
|
|
1513
|
+
const source = this.sourceFingerprint(profileHome);
|
|
1259
1514
|
const key = `${profileHome}\0${lookup}`;
|
|
1260
1515
|
try {
|
|
1261
1516
|
const sidecar = this.deps.sidecar();
|
|
1262
1517
|
if (!sidecar)
|
|
1263
1518
|
throw new Error('the sidecar is not running');
|
|
1264
1519
|
const answer = await call(sidecar);
|
|
1265
|
-
this.
|
|
1266
|
-
|
|
1520
|
+
if (source !== this.sourceFingerprint(profileHome))
|
|
1521
|
+
return { value: null, authoritative: false };
|
|
1522
|
+
this.lastPoolAnswers.set(key, { source, value: answer });
|
|
1523
|
+
return { value: answer, authoritative: true };
|
|
1267
1524
|
}
|
|
1268
1525
|
catch (error) {
|
|
1269
1526
|
this.deps.log(`[catalog] credential pool lookup ${lookup.replace('\0', ' ')} failed: ${str(error.message)}`);
|
|
1270
|
-
|
|
1527
|
+
const hit = this.lastPoolAnswers.get(key);
|
|
1528
|
+
return { value: hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.value : null, authoritative: false };
|
|
1271
1529
|
}
|
|
1272
1530
|
}
|
|
1273
|
-
/**
|
|
1531
|
+
/** One shared read of pool membership and scoped key revisions before reusing catalog data. */
|
|
1274
1532
|
poolProviders(profileHome) {
|
|
1275
|
-
|
|
1533
|
+
const inputSource = this.sourceFingerprint(profileHome, false);
|
|
1534
|
+
const readKey = this.sourceFingerprint(profileHome);
|
|
1535
|
+
const pending = this.credentialReads.get(readKey);
|
|
1536
|
+
if (pending)
|
|
1537
|
+
return pending;
|
|
1538
|
+
const read = (async () => {
|
|
1539
|
+
const config = await this.deps.config.read(profileHome);
|
|
1540
|
+
const key_envs = Object.fromEntries(BALANCE_PROVIDERS.filter((pid) => this.apiKeyFor(pid, profileHome, config) === null).map((pid) => [pid, this.usageKey(pid, null).env_vars ?? []]));
|
|
1541
|
+
const result = await this.poolLookup(profileHome, 'providers', (sidecar) => sidecar.call('usage.pool_providers', { profile_home: profileHome, key_envs }));
|
|
1542
|
+
if (inputSource !== this.sourceFingerprint(profileHome, false))
|
|
1543
|
+
return { value: null, authoritative: false };
|
|
1544
|
+
this.scopedKeyAuthority.set(profileHome, result.authoritative);
|
|
1545
|
+
if (result.authoritative)
|
|
1546
|
+
this.scopedKeyRevisions.set(profileHome, JSON.stringify(Object.entries(result.value?.keys ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([pid, state]) => [pid, state.revision])));
|
|
1547
|
+
return result;
|
|
1548
|
+
})().catch(() => {
|
|
1549
|
+
if (inputSource === this.sourceFingerprint(profileHome, false))
|
|
1550
|
+
this.scopedKeyAuthority.set(profileHome, false);
|
|
1551
|
+
return { value: null, authoritative: false };
|
|
1552
|
+
}).finally(() => { this.credentialReads.delete(readKey); });
|
|
1553
|
+
this.credentialReads.set(readKey, read);
|
|
1554
|
+
return read;
|
|
1276
1555
|
}
|
|
1277
1556
|
/** TAL-548: one key-based balance read through the sidecar, which resolves a pool account's key itself; null when it fails. */
|
|
1278
1557
|
async balance(profileHome, provider, key) {
|
|
@@ -1289,52 +1568,46 @@ export class ProviderCatalog {
|
|
|
1289
1568
|
}
|
|
1290
1569
|
quotaScopeCache = null;
|
|
1291
1570
|
/** Python `_quota_server_scope_id`: a 32-hex id persisted at `<state>/.quota_scope_id` (created atomically once). */
|
|
1292
|
-
quotaServerScopeId() {
|
|
1571
|
+
async quotaServerScopeId() {
|
|
1293
1572
|
if (this.quotaScopeCache)
|
|
1294
1573
|
return this.quotaScopeCache;
|
|
1295
|
-
const
|
|
1296
|
-
if (!
|
|
1574
|
+
const documents = this.deps.documents;
|
|
1575
|
+
if (!documents) {
|
|
1297
1576
|
this.quotaScopeCache = randomUUID().replaceAll('-', '');
|
|
1298
1577
|
return this.quotaScopeCache;
|
|
1299
1578
|
}
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1308
|
-
if (!/^[0-9a-f]{32}$/.test(stored)) {
|
|
1309
|
-
stored = randomUUID().replaceAll('-', '');
|
|
1310
|
-
mkdirSync(dir, { recursive: true });
|
|
1311
|
-
const temporary = `${path}.tmp.${String(process.pid)}`;
|
|
1312
|
-
writeFileSync(temporary, `${stored}\n`, 'utf8');
|
|
1313
|
-
renameSync(temporary, path);
|
|
1314
|
-
}
|
|
1579
|
+
const stored = await updateDocument(documents, 'catalog', 'quota-scope', (value) => {
|
|
1580
|
+
const previous = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1581
|
+
if (/^[0-9a-f]{32}$/.test(previous))
|
|
1582
|
+
return { value, result: previous };
|
|
1583
|
+
const id = randomUUID().replaceAll('-', '');
|
|
1584
|
+
return { value: `${id}\n`, result: id };
|
|
1585
|
+
});
|
|
1315
1586
|
this.quotaScopeCache = stored;
|
|
1316
1587
|
return stored;
|
|
1317
1588
|
}
|
|
1318
1589
|
/** Python `_quota_profile_scope_id`. */
|
|
1319
|
-
quotaProfileScopeId(profile) {
|
|
1320
|
-
return `qscope_${createHash('sha256').update(`${this.quotaServerScopeId()}\0${profile}`).digest('hex').slice(0, 32)}`;
|
|
1590
|
+
async quotaProfileScopeId(profile) {
|
|
1591
|
+
return `qscope_${createHash('sha256').update(`${(await this.quotaServerScopeId())}\0${profile}`).digest('hex').slice(0, 32)}`;
|
|
1321
1592
|
}
|
|
1322
1593
|
/**
|
|
1323
1594
|
* Python `get_provider_quotas`: the stable-identity envelope the iOS widget persists. A keyed provider is one source per
|
|
1324
1595
|
* credential-pool account, else one for its own key (Python `_quota_source_descriptors`, TAL-548). Each source id appears
|
|
1325
1596
|
* once (two custom providers can share a slug), ordered by provider, account label, then source id.
|
|
1326
1597
|
*/
|
|
1327
|
-
async quotas(profileHome, profile, opts = {}) {
|
|
1598
|
+
async quotas(profileHome, profile, opts = {}, attempt = 0) {
|
|
1599
|
+
const sequence = ++this.quotaReadSequence;
|
|
1600
|
+
const source = this.sourceFingerprint(profileHome);
|
|
1328
1601
|
const at = this.deps.now();
|
|
1329
1602
|
const { payload: status, pooled: pooledIds } = await this.providerRows(profileHome);
|
|
1330
1603
|
const active = status.active_provider;
|
|
1331
|
-
const scopeId = this.quotaProfileScopeId(profile);
|
|
1604
|
+
const scopeId = (await this.quotaProfileScopeId(profile));
|
|
1332
1605
|
// Python `_quota_source_id(profile, provider, credential_id)`: a pool account's id, or "provider" for the provider's own key.
|
|
1333
1606
|
const sourceId = (pid, credential) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\0${credential}`).digest('hex').slice(0, 32)}`;
|
|
1334
1607
|
// A provider configured only through `hermes auth add` has no key in .env or config.yaml, yet its pool accounts are
|
|
1335
1608
|
// sources, including a signed-out OAuth provider's. An unknown pool (a failed lookup with no earlier answer) never
|
|
1336
1609
|
// reports a requested source as missing.
|
|
1337
|
-
let poolUnknown =
|
|
1610
|
+
let poolUnknown = !status.availability.authoritative;
|
|
1338
1611
|
const pooled = new Set(pooledIds ?? []);
|
|
1339
1612
|
const config = await this.deps.config.read(profileHome);
|
|
1340
1613
|
const perProvider = await Promise.all(status.providers.filter((p) => p.has_key || p.is_custom || pooled.has(str(p.id))).map(async (p) => {
|
|
@@ -1342,30 +1615,80 @@ export class ProviderCatalog {
|
|
|
1342
1615
|
const label = str(p.display_name) || pid;
|
|
1343
1616
|
const apiKey = this.apiKeyFor(pid, profileHome, config);
|
|
1344
1617
|
const found = await this.poolEntries(profileHome, pid, apiKey);
|
|
1345
|
-
if (!found)
|
|
1618
|
+
if (!found.authoritative)
|
|
1346
1619
|
poolUnknown = true;
|
|
1347
|
-
const entries = found ?? [];
|
|
1620
|
+
const entries = found.value?.entries ?? [];
|
|
1348
1621
|
const accounts = entries.map((e) => ({ source_id: sourceId(pid, e.credential_id), provider_id: pid, provider_label: label, account_label: e.label, credential: e }));
|
|
1349
1622
|
// The provider's own key is a source unless a pool account already holds it.
|
|
1350
|
-
const ownKey = !entries.length || (apiKey !== null && !entries.some((e) => e.matches_api_key));
|
|
1623
|
+
const ownKey = found.authoritative && ((!entries.length && (p.has_key === true || p.is_custom === true) && !pooled.has(pid)) || ((apiKey !== null || found.value?.has_api_key === true) && !entries.some((e) => e.matches_api_key)));
|
|
1351
1624
|
return ownKey ? [{ source_id: sourceId(pid, 'provider'), provider_id: pid, provider_label: label, account_label: label, credential: null }, ...accounts] : accounts;
|
|
1352
1625
|
}));
|
|
1353
1626
|
let descriptors = uniqueQuotaSources(perProvider.flat());
|
|
1627
|
+
const membership = new Set(descriptors.map((d) => d.source_id));
|
|
1354
1628
|
const requested = str(opts.sourceId).trim() || null;
|
|
1355
1629
|
if (requested)
|
|
1356
1630
|
descriptors = descriptors.filter((d) => d.source_id === requested);
|
|
1357
|
-
const thresholds = this.deps.quotaThresholds?.(profile) ?? QUOTA_THRESHOLD_DEFAULTS;
|
|
1358
|
-
|
|
1359
|
-
const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at, credential: d.credential });
|
|
1631
|
+
const thresholds = (await this.deps.quotaThresholds?.(profile)) ?? QUOTA_THRESHOLD_DEFAULTS;
|
|
1632
|
+
let sources = await Promise.all(descriptors.map(async (d) => {
|
|
1633
|
+
const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at, credential: d.credential, sequence });
|
|
1360
1634
|
const limits = dict(q.account_limits);
|
|
1361
1635
|
return classifyQuotaSource({
|
|
1362
|
-
source_id: d.source_id, provider_id: d.provider_id, provider_label: d.provider_label, account_label: d.account_label,
|
|
1636
|
+
computed_at: isoAt(at), source_id: d.source_id, provider_id: d.provider_id, provider_label: d.provider_label, account_label: d.account_label,
|
|
1363
1637
|
is_active_provider: d.provider_id === active, supported: q.supported === true, status: str(limits.status) || str(q.status) || 'unavailable',
|
|
1364
1638
|
plan: limits.plan ?? null, windows: (limits.windows ?? []), pace_window_index: (limits.pace_window_index ?? null), session_window_index: limits.session_window_index ?? null, weekly_window_index: limits.weekly_window_index ?? null, quota: q.quota ?? null, balances: q.balances ?? [], details: limits.details ?? [],
|
|
1365
1639
|
unavailable_reason: limits.unavailable_reason ?? null, retry_after: limits.retry_after ?? null, fetched_at: limits.fetched_at ?? null, message: q.message ?? null,
|
|
1366
1640
|
}, thresholds);
|
|
1367
1641
|
}));
|
|
1368
|
-
|
|
1642
|
+
if (source !== this.sourceFingerprint(profileHome) && attempt === 0)
|
|
1643
|
+
return this.quotas(profileHome, profile, opts, 1);
|
|
1644
|
+
poolUnknown ||= source !== this.sourceFingerprint(profileHome);
|
|
1645
|
+
const cacheKey = `${profileHome}\0${scopeId}`;
|
|
1646
|
+
const cached = source === this.sourceFingerprint(profileHome) ? this.lastQuotaSources.get(cacheKey) : undefined;
|
|
1647
|
+
const prior = cached?.source === source ? cached : undefined;
|
|
1648
|
+
const previous = prior?.sources ?? [];
|
|
1649
|
+
if (!poolUnknown) {
|
|
1650
|
+
sources = sources.map((s) => {
|
|
1651
|
+
const old = previous.find((p) => p.source_id === s.source_id);
|
|
1652
|
+
return s.status === 'unavailable' && old ? this.staleQuotaSource(old, str(s.unavailable_reason || s.message)) : s;
|
|
1653
|
+
});
|
|
1654
|
+
const latestMembership = prior && prior.sequence > sequence ? prior.membership : membership;
|
|
1655
|
+
const sourceSequences = new Map(prior?.sourceSequences);
|
|
1656
|
+
const merged = new Map(previous.filter((s) => latestMembership.has(str(s.source_id))).map((s) => [str(s.source_id), s]));
|
|
1657
|
+
for (const value of sources) {
|
|
1658
|
+
const id = str(value.source_id);
|
|
1659
|
+
if (!latestMembership.has(id) || (sourceSequences.get(id) ?? -1) > sequence)
|
|
1660
|
+
continue;
|
|
1661
|
+
merged.set(id, value);
|
|
1662
|
+
sourceSequences.set(id, sequence);
|
|
1663
|
+
}
|
|
1664
|
+
for (const id of sourceSequences.keys())
|
|
1665
|
+
if (!latestMembership.has(id))
|
|
1666
|
+
sourceSequences.delete(id);
|
|
1667
|
+
const snapshot = uniqueQuotaSources([...merged.values()]);
|
|
1668
|
+
this.lastQuotaSources.set(cacheKey, { source, sequence: Math.max(sequence, prior?.sequence ?? -1), membership: latestMembership, sources: snapshot, sourceSequences });
|
|
1669
|
+
// A delayed read answers with the newest confirmed projection, including confirmed removals.
|
|
1670
|
+
sources = snapshot.filter((s) => !requested || s.source_id === requested);
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
sources = previous.filter((s) => !requested || s.source_id === requested).map((s) => this.staleQuotaSource(s));
|
|
1674
|
+
}
|
|
1675
|
+
const currentSource = this.sourceFingerprint(profileHome);
|
|
1676
|
+
if (currentSource !== source) {
|
|
1677
|
+
poolUnknown = true;
|
|
1678
|
+
sources = [];
|
|
1679
|
+
}
|
|
1680
|
+
const latestMembership = this.lastQuotaSources.get(cacheKey)?.membership;
|
|
1681
|
+
const availability = this.availability(currentSource, !poolUnknown, sources.length > 0);
|
|
1682
|
+
return { availability, version: 1, computed_at: isoAt(at), scope_id: scopeId, profile_id: profile, active_provider: active, requested_source_id: requested, missing_source: Boolean(requested && !poolUnknown && !latestMembership?.has(requested)), sources };
|
|
1683
|
+
}
|
|
1684
|
+
lastQuotaSources = new Map();
|
|
1685
|
+
staleQuotaSource(source, reason = 'Provider data is temporarily unavailable. Showing last-confirmed data.') {
|
|
1686
|
+
return { ...source, status: 'stale', unavailable_reason: reason, message: reason, urgency: { remaining: 'stale', pace: 'stale' } };
|
|
1687
|
+
}
|
|
1688
|
+
availability(source, authoritative, cached) {
|
|
1689
|
+
return { authoritative, status: authoritative ? 'available' : cached ? 'stale' : 'unavailable',
|
|
1690
|
+
unavailable_reason: authoritative ? null : cached ? 'Provider data is temporarily unavailable. Showing last-confirmed data.' : 'Provider data is temporarily unavailable. Try again.',
|
|
1691
|
+
generation: createHash('sha256').update(source).digest('hex') };
|
|
1369
1692
|
}
|
|
1370
1693
|
/** Python `get_provider_cost_history` (OpenRouter only; daily snapshots under `<home>/cost-snapshots`). */
|
|
1371
1694
|
async costHistory(profileHome, providerRaw, days) {
|
|
@@ -1378,9 +1701,9 @@ export class ProviderCatalog {
|
|
|
1378
1701
|
const budget = this.deps.costBudget();
|
|
1379
1702
|
const config = await this.deps.config.read(profileHome);
|
|
1380
1703
|
const apiKey = this.apiKeyFor('openrouter', profileHome, config);
|
|
1381
|
-
|
|
1704
|
+
const info = await this.balance(profileHome, 'openrouter', this.usageKey('openrouter', apiKey));
|
|
1705
|
+
if (info?.status === 'no_key')
|
|
1382
1706
|
return { ok: false, provider, display_name: name, supported: true, status: 'no_key', monthly_budget: budget, message: 'OpenRouter cost history needs an OPENROUTER_API_KEY configured on the server.' };
|
|
1383
|
-
const info = await this.balance(profileHome, 'openrouter', { api_key: apiKey });
|
|
1384
1707
|
if (info?.status !== 'ok' || !info.quota) {
|
|
1385
1708
|
return { ok: false, provider, display_name: name, supported: true, status: 'unavailable', window_days: days, ...costHistoryView(this.readCostSnapshots(profileHome), days, budget), limit: null, label: null, monthly_budget: budget, message: 'OpenRouter cost history is temporarily unavailable. Showing last known data.' };
|
|
1386
1709
|
}
|