@maudecode/talaria-web 1.18.0 → 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.js +16 -16
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +2 -2
- package/dist/api/automation-raw.js +32 -32
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +102 -22
- package/dist/api/automation-router.js +15 -13
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +1205 -34
- package/dist/api/chat-router.js +158 -78
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +2 -2
- package/dist/api/git-router.js +29 -29
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +3 -3
- package/dist/api/raw-routes.js +63 -61
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +6 -6
- package/dist/api/router.js +22 -22
- 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 +1928 -124
- package/dist/api/sessions-router.js +177 -173
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +427 -9
- package/dist/api/settings-router.js +48 -32
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +2 -2
- package/dist/api/sse-routes.js +63 -63
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.d.ts +5 -0
- package/dist/api/tools-raw.js +58 -33
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +663 -91
- package/dist/api/tools-router.js +309 -52
- 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 +66 -47
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +10 -9
- package/dist/auth/gate.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 +14 -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 +113 -58
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +14 -5
- 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 +14 -9
- package/dist/http/context.js +29 -20
- package/dist/http/context.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 +10 -4
- package/dist/profiles/profiles.js +77 -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 +78 -12
- package/dist/providers/catalog.js +445 -141
- 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/redact.js +18 -9
- 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 +233 -90
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +10 -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 -20
- package/dist/sessions/background-tasks.js +88 -86
- 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 +15 -5
- package/dist/sessions/cli-sessions.js +62 -43
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +30 -2
- package/dist/sessions/completions.js +294 -53
- 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/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/goals.d.ts +31 -0
- package/dist/sessions/goals.js +75 -0
- package/dist/sessions/goals.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 +27 -18
- package/dist/sessions/list.js +133 -131
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +68 -10
- package/dist/sessions/merge.js +259 -51
- 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 +4 -7
- package/dist/sessions/recovery.js +97 -281
- package/dist/sessions/recovery.js.map +1 -1
- package/dist/sessions/relay.d.ts +20 -20
- package/dist/sessions/relay.js +205 -127
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +162 -59
- package/dist/sessions/service.js +1133 -521
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +18 -2
- package/dist/sessions/session.js +29 -5
- 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.js +3 -1
- package/dist/sessions/state-db-worker.js.map +1 -1
- package/dist/sessions/state-db.d.ts +53 -8
- package/dist/sessions/state-db.js +576 -50
- 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 +63 -73
- package/dist/sessions/store.js +305 -611
- 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.js +1 -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 +22 -16
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +10 -1
- package/dist/sessions/turn-context.js +7 -4
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +87 -17
- package/dist/sessions/turn.js +795 -347
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +8 -13
- package/dist/sessions/wakeup-pause.js +31 -49
- package/dist/sessions/wakeup-pause.js.map +1 -1
- 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 +179 -183
- 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.js +19 -2
- package/dist/sidecar/fake.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/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 +82 -15
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +5 -5
- package/dist/tools/dashboard-plugins.js +14 -14
- package/dist/tools/dashboard-plugins.js.map +1 -1
- package/dist/tools/extensions.d.ts +5 -4
- package/dist/tools/extensions.js +213 -207
- 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 +15 -3
- package/dist/tools/health.js +97 -34
- 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/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/skills.d.ts +9 -4
- package/dist/tools/skills.js +44 -13
- 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 +93 -33
- package/dist/tools/updates.js +1051 -345
- package/dist/tools/updates.js.map +1 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +5 -0
- package/dist/util.js.map +1 -1
- package/dist/workspace/git.d.ts +16 -1
- package/dist/workspace/git.js +291 -11
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +12 -9
- package/dist/workspace/media.js +31 -20
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/rollback.d.ts +2 -2
- package/dist/workspace/rollback.js +5 -5
- package/dist/workspace/rollback.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.js +170 -32
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +5 -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 +8 -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 +43 -1
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +139 -31
- package/sidecar/talaria_sidecar/methods/chat.py +847 -134
- package/sidecar/talaria_sidecar/methods/commands.py +213 -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 +366 -34
- package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
- package/sidecar/talaria_sidecar/methods/gateway.py +194 -54
- 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 +80 -27
- 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 +160 -11
- package/sidecar/talaria_sidecar/methods/profiles.py +44 -6
- package/sidecar/talaria_sidecar/methods/providers.py +196 -26
- 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 +277 -70
- 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 +81 -12
- package/sidecar/talaria_sidecar/speed.py +35 -0
- package/static/dist/FILES.txt +126 -127
- 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-gfbzpoou.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-fn1bhde6.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-c8kdixke.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-cku06x1p.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-jzthp1wa.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
- package/static/dist/assets/channel-es95o4y8.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-ofajdixh.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-e7gkikcw.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.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-magtofyp.js → copy-jamtd1pf.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-siqlulkl.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-i648tah9.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-mwn6uc17.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-mekq7zgq.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-xe6r5qfw.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-nq07dg7k.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-c62ipm3g.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
- package/static/dist/assets/{erDiagram-RLTQ6QDP-h39kmnzy.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-ngbsyp37.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-fr2i7caa.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-jwpop9p8.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-h11x0dxo.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-dfh47dw9.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
- package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
- package/static/dist/assets/{line-meizqsvn.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-ntne45i9.js → mermaid-parser.core-bnre27u8.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.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-l6jdka8p.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
- package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
- package/static/dist/assets/{pegDiagram-XKGWAZYB-g1er3bxj.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-i0ylcrt9.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-fbz1e55z.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-f9b70aa4.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-lgsuoeda.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-eqpy9fnz.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
- package/static/dist/assets/{refresh-cw-hlwb02qv.js → refresh-cw-mqbkv2a3.js} +1 -1
- package/static/dist/assets/refresh-l29ljp76.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ogf6puhi.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
- package/static/dist/assets/saved-ndd4t2xb.js +1 -0
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.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-fyoznpcs.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-oefldhfy.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
- package/static/dist/assets/toast-i3h34v6o.js +64 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-f7hcci5o.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-nl85071a.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-b4dq6n4q.js → useParams-gjc70qru.js} +1 -1
- package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-e6ae539u.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-h5hzbk0n.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-jftvpm3e.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-hedh85ge.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.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-g3bkgnvt.js +0 -1
- package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
- package/static/dist/assets/Dialog-g355qxhp.js +0 -1
- package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
- package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
- package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
- package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
- package/static/dist/assets/Select-kddq75qc.js +0 -1
- package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
- package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
- package/static/dist/assets/States-eq6fe29m.js +0 -1
- package/static/dist/assets/Toaster-pc247u9f.js +0 -1
- package/static/dist/assets/_app-f8n1i0m1.js +0 -1
- package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
- package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
- package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
- package/static/dist/assets/_app.logs-i69qds98.js +0 -1
- package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
- package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
- package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
- package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
- package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
- package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
- package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
- package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
- package/static/dist/assets/back-h7aun7ew.js +0 -1
- package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
- package/static/dist/assets/channel-b2zo73z1.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
- package/static/dist/assets/copied-hcplv46p.js +0 -1
- package/static/dist/assets/endpoints-klbxod4t.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
- package/static/dist/assets/host-i828xuo7.js +0 -1
- package/static/dist/assets/index-eh8zilnv.css +0 -2
- package/static/dist/assets/index-r648f4o1.js +0 -11
- package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
- package/static/dist/assets/link-oot2yn73.js +0 -1
- package/static/dist/assets/login-gu28l9ou.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
- package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
- package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
- package/static/dist/assets/refresh-abtu93ke.js +0 -1
- package/static/dist/assets/saved-girnabkf.js +0 -1
- package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
- package/static/dist/assets/share._token-kl0r7ido.js +0 -2
- package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
- package/static/dist/assets/sse-iwku0sjo.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
- package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
- package/static/dist/assets/units-kgxzxfhi.js +0 -1
- package/static/dist/assets/untitled-cikfjntn.js +0 -1
- package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
- package/static/dist/assets/useLocale-o59tbiea.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
|
});
|
|
@@ -167,7 +161,13 @@ const unqualifiedModelId = (id) => parseProviderQualifiedModel(id)?.[0] ?? id.tr
|
|
|
167
161
|
const MODEL_PICKER_OVERFLOW_THRESHOLD = 25;
|
|
168
162
|
const MODEL_PICKER_VISIBLE_TARGET = 15;
|
|
169
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;
|
|
170
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;
|
|
171
171
|
const PROVIDERS_TTL_S = 30;
|
|
172
172
|
/** The sidecar reads a balance endpoint with a 15 s timeout of its own. */
|
|
173
173
|
const BALANCE_TIMEOUT_MS = 20_000;
|
|
@@ -343,6 +343,11 @@ export function classifyQuotaSource(source, t) {
|
|
|
343
343
|
urgency: { remaining: urgency(source.windows[0]).remaining, pace: urgency(source.windows[source.pace_window_index ?? 0]).pace },
|
|
344
344
|
};
|
|
345
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
|
+
}
|
|
346
351
|
export function displayName(pid) {
|
|
347
352
|
return PROVIDER_DISPLAY[pid] ?? pid.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ');
|
|
348
353
|
}
|
|
@@ -391,6 +396,10 @@ function looksLikeCodexOauthToken(value) {
|
|
|
391
396
|
return false;
|
|
392
397
|
}
|
|
393
398
|
}
|
|
399
|
+
const BALANCE_PROVIDERS = ['openrouter', 'deepseek', 'opencode-go'];
|
|
400
|
+
function isBalanceProvider(pid) {
|
|
401
|
+
return BALANCE_PROVIDERS.some((provider) => provider === pid);
|
|
402
|
+
}
|
|
394
403
|
function valueCountsAsApiKey(pid, value) {
|
|
395
404
|
const text = str(value).trim();
|
|
396
405
|
if (!text)
|
|
@@ -458,9 +467,12 @@ export function uniqueQuotaSources(sources) {
|
|
|
458
467
|
}
|
|
459
468
|
export class ProviderCatalog {
|
|
460
469
|
deps;
|
|
470
|
+
scopedKeyRevisions = new Map();
|
|
471
|
+
scopedKeyAuthority = new Map();
|
|
472
|
+
credentialReads = new Map();
|
|
461
473
|
/** Failed live lookups by home, provider, and the `sourceFingerprint` they ran under (TAL-542). */
|
|
462
474
|
liveFailed = new Set();
|
|
463
|
-
/** `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. */
|
|
464
476
|
liveIds = new Map();
|
|
465
477
|
liveInflight = new Map();
|
|
466
478
|
/** `pooled`: the fill's `poolProviders` answer, null when unknown (TAL-638). */
|
|
@@ -482,15 +494,23 @@ export class ProviderCatalog {
|
|
|
482
494
|
}
|
|
483
495
|
return { groups: this.liveIds.size, models, age_seconds: oldest === null ? null : Math.max(0, Math.round(this.deps.now() - oldest)) };
|
|
484
496
|
}
|
|
485
|
-
invalidate(profileHome, provider) {
|
|
497
|
+
invalidate(profileHome, provider, opts = {}) {
|
|
486
498
|
for (const key of [...this.liveIds.keys()]) {
|
|
487
499
|
if (profileHome && !key.startsWith(`${profileHome}\0`))
|
|
488
500
|
continue;
|
|
489
501
|
if (provider && !key.endsWith(`\0${provider}`))
|
|
490
502
|
continue;
|
|
491
|
-
|
|
503
|
+
if (opts.preserveConfirmed)
|
|
504
|
+
this.liveIds.get(key).ttl = 0;
|
|
505
|
+
else
|
|
506
|
+
this.liveIds.delete(key);
|
|
492
507
|
}
|
|
493
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);
|
|
494
514
|
// Credentials changed: the last pool answers no longer stand in for a failed lookup.
|
|
495
515
|
for (const key of [...this.lastPoolAnswers.keys()])
|
|
496
516
|
if (!profileHome || key.startsWith(`${profileHome}\0`))
|
|
@@ -514,27 +534,44 @@ export class ProviderCatalog {
|
|
|
514
534
|
}
|
|
515
535
|
return catalogOptionId(catalog, model, provider);
|
|
516
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
|
+
}
|
|
517
544
|
/**
|
|
518
545
|
* TAL-542: the identity of the files a catalog and its live ids derive from: config.yaml, `.env`, the Agent's sign-in
|
|
519
546
|
* store (`auth.json`, rewritten by an account switch), and the installed plugins directory, so a change outside Web
|
|
520
|
-
* makes both stale.
|
|
547
|
+
* makes both stale. Opaque scoped-key revisions cover external secret sources without exposing their keys.
|
|
521
548
|
*/
|
|
522
|
-
sourceFingerprint(profileHome) {
|
|
549
|
+
sourceFingerprint(profileHome, includeScoped = true) {
|
|
523
550
|
const stamp = (name) => {
|
|
524
551
|
try {
|
|
525
552
|
const st = statSync(join(profileHome, name), { bigint: true });
|
|
526
|
-
return `${String(st.mtimeNs)}:${String(st.size)}:${String(st.ino)}`;
|
|
553
|
+
return `${String(st.mtimeNs)}:${String(st.ctimeNs)}:${String(st.size)}:${String(st.ino)}`;
|
|
527
554
|
}
|
|
528
555
|
catch {
|
|
529
556
|
return 'missing';
|
|
530
557
|
}
|
|
531
558
|
};
|
|
532
|
-
|
|
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('|');
|
|
533
570
|
}
|
|
534
571
|
/** The last catalog built for this home, younger than `maxAgeS`, while its source files are unchanged since (TAL-542). */
|
|
535
572
|
currentModels(profileHome, maxAgeS = Infinity) {
|
|
536
573
|
const meta = this.lastModelsMeta.get(profileHome);
|
|
537
|
-
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;
|
|
538
575
|
}
|
|
539
576
|
/**
|
|
540
577
|
* TAL-542: `repairSessionModel` against this home's catalog. Agent-owned sign-in and plugin state has no file to
|
|
@@ -554,6 +591,7 @@ export class ProviderCatalog {
|
|
|
554
591
|
* await. A source that changed while the build awaited makes that build stale, so it is rebuilt once.
|
|
555
592
|
*/
|
|
556
593
|
async warmSessionModelRepair(profileHome) {
|
|
594
|
+
await this.poolProviders(profileHome);
|
|
557
595
|
for (let attempt = 0; attempt < 2 && !this.currentModels(profileHome, PROVIDERS_TTL_S); attempt += 1) {
|
|
558
596
|
try {
|
|
559
597
|
await this.models(profileHome);
|
|
@@ -565,6 +603,7 @@ export class ProviderCatalog {
|
|
|
565
603
|
}
|
|
566
604
|
/** Builds the catalog `modelOptionFor` reads once per home, again after config.yaml or `.env` changes; an unavailable catalog leaves every option id null. */
|
|
567
605
|
async warmModelOptions(profileHome) {
|
|
606
|
+
await this.poolProviders(profileHome);
|
|
568
607
|
if (this.currentModels(profileHome))
|
|
569
608
|
return;
|
|
570
609
|
try {
|
|
@@ -613,18 +652,14 @@ export class ProviderCatalog {
|
|
|
613
652
|
return false;
|
|
614
653
|
}
|
|
615
654
|
async authStatus(profileHome, pid) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return
|
|
619
|
-
|
|
620
|
-
return (await sidecar.call('providers.auth_status', { profile_home: profileHome, provider: pid })).status;
|
|
621
|
-
}
|
|
622
|
-
catch {
|
|
623
|
-
return null;
|
|
624
|
-
}
|
|
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
|
+
});
|
|
625
659
|
}
|
|
626
660
|
/** `pooled`: the providers whose persisted credential pool holds an added account (TAL-638). */
|
|
627
|
-
async probeKey(profileHome, pid, config, envValues, pooled) {
|
|
661
|
+
async probeKey(profileHome, pid, config, envValues, pooled, scopedKeys) {
|
|
662
|
+
let authoritative = true;
|
|
628
663
|
let hasKey = this.providerHasKey(pid, config, envValues, profileHome);
|
|
629
664
|
let isOauth = OAUTH_PROVIDERS.has(pid);
|
|
630
665
|
let keySource = 'none';
|
|
@@ -632,7 +667,9 @@ export class ProviderCatalog {
|
|
|
632
667
|
let removable = false;
|
|
633
668
|
if (isOauth) {
|
|
634
669
|
keySource = 'oauth';
|
|
635
|
-
const
|
|
670
|
+
const lookup = await this.authStatus(profileHome, pid);
|
|
671
|
+
authoritative = lookup.authoritative;
|
|
672
|
+
const status = lookup.value;
|
|
636
673
|
if (status?.logged_in) {
|
|
637
674
|
hasKey = true;
|
|
638
675
|
keySource = str(status.key_source) || 'oauth';
|
|
@@ -677,8 +714,16 @@ export class ProviderCatalog {
|
|
|
677
714
|
hasKey = true;
|
|
678
715
|
keySource = 'credential_pool';
|
|
679
716
|
}
|
|
717
|
+
else if (isBalanceProvider(pid)) {
|
|
718
|
+
if (scopedKeys[pid]?.present) {
|
|
719
|
+
hasKey = true;
|
|
720
|
+
keySource = 'env';
|
|
721
|
+
}
|
|
722
|
+
}
|
|
680
723
|
else if (!providerEnvVar(pid) && /^[a-z][a-z0-9_-]{0,63}$/.test(pid)) {
|
|
681
|
-
const
|
|
724
|
+
const lookup = await this.authStatus(profileHome, pid);
|
|
725
|
+
authoritative = lookup.authoritative;
|
|
726
|
+
const status = lookup.value;
|
|
682
727
|
if (status?.logged_in) {
|
|
683
728
|
hasKey = true;
|
|
684
729
|
const raw = str(status.key_source);
|
|
@@ -686,14 +731,21 @@ export class ProviderCatalog {
|
|
|
686
731
|
isOauth = true;
|
|
687
732
|
}
|
|
688
733
|
}
|
|
689
|
-
return { hasKey, keySource, authError, isOauth, removable };
|
|
734
|
+
return { authoritative, hasKey, keySource, authError, isOauth, removable };
|
|
690
735
|
}
|
|
691
736
|
/** Live model ids from the Agent for one provider, cached per profile home. */
|
|
692
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);
|
|
693
741
|
const key = `${profileHome}\0${pid}`;
|
|
694
|
-
const source = this.sourceFingerprint(profileHome);
|
|
742
|
+
const source = inputSource === this.sourceFingerprint(profileHome, false) ? this.sourceFingerprint(profileHome) : inputSource;
|
|
695
743
|
const hit = this.liveIds.get(key);
|
|
696
|
-
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)
|
|
697
749
|
return hit.ids;
|
|
698
750
|
// A lookup started under another source answers for the old endpoint or credential; this one starts its own.
|
|
699
751
|
const flightKey = `${key}\0${source}`;
|
|
@@ -704,51 +756,75 @@ export class ProviderCatalog {
|
|
|
704
756
|
// No sidecar is a failed lookup for this source: the ids it keeps showing never confirm a stale-model repair (TAL-542).
|
|
705
757
|
if (!sidecar) {
|
|
706
758
|
this.liveFailed.add(flightKey);
|
|
707
|
-
return hit?.ids
|
|
759
|
+
return hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.ids : [];
|
|
708
760
|
}
|
|
709
761
|
const run = sidecar.call('providers.model_ids', { profile_home: profileHome, provider: pid, ...(opts.force ? { force_refresh: true } : {}) }, { timeoutMs: 30_000 })
|
|
710
|
-
.then((r) => {
|
|
711
|
-
|
|
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 : []; })
|
|
712
780
|
.finally(() => { this.liveInflight.delete(flightKey); });
|
|
713
781
|
this.liveInflight.set(flightKey, run);
|
|
714
782
|
return run;
|
|
715
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
|
+
}
|
|
716
788
|
/**
|
|
717
789
|
* TAL-288: the model-provider plugins installed and enabled in this profile, as the sidecar reads them from the Agent.
|
|
718
790
|
* A failed lookup lists none, so every other provider stays usable; an id that cannot be a provider routing hint is dropped.
|
|
719
791
|
*/
|
|
720
792
|
async pluginProviders(profileHome) {
|
|
721
|
-
|
|
722
|
-
if (!sidecar)
|
|
723
|
-
return [];
|
|
724
|
-
try {
|
|
793
|
+
return this.poolLookup(profileHome, 'plugins', async (sidecar) => {
|
|
725
794
|
const { providers } = await sidecar.call('plugins.providers', { profile_home: profileHome }, { timeoutMs: PLUGIN_LIST_TIMEOUT_MS });
|
|
726
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);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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 }));
|
|
732
804
|
}
|
|
733
805
|
/** Python `get_providers`. */
|
|
734
806
|
async providers(profileHome) {
|
|
735
807
|
return structuredClone((await this.providerRows(profileHome)).payload);
|
|
736
808
|
}
|
|
737
809
|
/** The cached `providers` answer, shared with `quotas`; callers never mutate it. */
|
|
738
|
-
async providerRows(profileHome) {
|
|
810
|
+
async providerRows(profileHome, pool) {
|
|
739
811
|
const config = await this.deps.config.read(profileHome);
|
|
740
812
|
const envValues = loadEnvFile(join(profileHome, '.env'));
|
|
741
|
-
|
|
813
|
+
pool ??= await this.poolProviders(profileHome);
|
|
814
|
+
const source = this.sourceFingerprint(profileHome);
|
|
815
|
+
const cacheKey = source;
|
|
742
816
|
const hit = this.providersCache.get(profileHome);
|
|
743
|
-
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)
|
|
744
818
|
return hit;
|
|
745
|
-
const pooledIds =
|
|
819
|
+
const pooledIds = pool.value?.providers ?? null;
|
|
820
|
+
const pluginLookup = await this.pluginProviders(profileHome);
|
|
821
|
+
let authoritative = pool.authoritative && pluginLookup.authoritative;
|
|
746
822
|
const pooled = new Set(pooledIds ?? []);
|
|
747
823
|
const active = activeProviderFromConfig(config);
|
|
748
824
|
const known = new Set([...Object.keys(PROVIDER_DISPLAY), ...Object.keys(PROVIDER_MODELS), ...OAUTH_PROVIDERS]);
|
|
749
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
|
|
750
826
|
// does own a `providers.<id>` config entry.
|
|
751
|
-
const plugins = (
|
|
827
|
+
const plugins = (pluginLookup.value ?? []).filter((p) => !known.has(p.name) && !known.has(providerIdentity(p.name)));
|
|
752
828
|
const pluginIds = new Set(plugins.map((p) => p.name));
|
|
753
829
|
const providersCfg = dict(config.providers);
|
|
754
830
|
for (const key of Object.keys(providersCfg)) {
|
|
@@ -759,7 +835,8 @@ export class ProviderCatalog {
|
|
|
759
835
|
}
|
|
760
836
|
const rows = [];
|
|
761
837
|
for (const pid of [...known].sort()) {
|
|
762
|
-
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;
|
|
763
840
|
if (pid === 'openai' && !probe.hasKey && looksLikeCodexOauthToken(str(envValues[providerEnvVar('openai') ?? ''] ?? this.deps.env.OPENAI_API_KEY)))
|
|
764
841
|
continue;
|
|
765
842
|
const configured = probe.hasKey || selfHostedEndpoint(pid, config);
|
|
@@ -768,7 +845,7 @@ export class ProviderCatalog {
|
|
|
768
845
|
// Python: the card prefers the live catalog for every keyed provider (exactly like the picker), keeping the
|
|
769
846
|
// static list as the cold/failed-probe fallback; Nous renders a featured subset with the full count.
|
|
770
847
|
if (configured && (probe.isOauth || pid === 'lmstudio' || pid === 'nous' || !BESPOKE_CATALOG_PROVIDERS.has(pid))) {
|
|
771
|
-
const live = await this.liveModelIds(profileHome, pid);
|
|
848
|
+
const live = await this.liveModelIds(profileHome, pid, { source });
|
|
772
849
|
if (live.length) {
|
|
773
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, []) }));
|
|
774
851
|
modelsTotal = live.length;
|
|
@@ -843,20 +920,30 @@ export class ProviderCatalog {
|
|
|
843
920
|
row.key_source_kind = keySourceKind(row);
|
|
844
921
|
row.models_total = Math.max(Number(row.models_total), row.models.length);
|
|
845
922
|
}
|
|
846
|
-
|
|
847
|
-
this.
|
|
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);
|
|
848
928
|
return entry;
|
|
849
929
|
}
|
|
850
930
|
/** Python `get_available_models` (static catalog + live ids for keyed providers). */
|
|
851
|
-
async models(profileHome) {
|
|
931
|
+
async models(profileHome, attempt = 0) {
|
|
932
|
+
const pool = await this.poolProviders(profileHome);
|
|
852
933
|
const source = this.sourceFingerprint(profileHome);
|
|
934
|
+
const providerState = (await this.providerRows(profileHome, pool)).payload;
|
|
853
935
|
const unconfirmed = new Set();
|
|
854
936
|
const config = await this.deps.config.read(profileHome);
|
|
855
937
|
const envValues = loadEnvFile(join(profileHome, '.env'));
|
|
856
|
-
const active = activeProviderFromConfig(config);
|
|
857
938
|
const model = modelSection(config);
|
|
858
939
|
const defaultModel = effectiveDefaultModel(config, this.deps.env);
|
|
859
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;
|
|
860
947
|
const detected = new Set();
|
|
861
948
|
const configuredIds = new Map();
|
|
862
949
|
const append = (pid, id) => {
|
|
@@ -870,6 +957,8 @@ export class ProviderCatalog {
|
|
|
870
957
|
};
|
|
871
958
|
if (active)
|
|
872
959
|
detected.add(active);
|
|
960
|
+
if (local)
|
|
961
|
+
detected.add(local.provider_id);
|
|
873
962
|
const providersCfg = dict(config.providers);
|
|
874
963
|
const rawKeyFor = new Map();
|
|
875
964
|
for (const [key, value] of Object.entries(providersCfg)) {
|
|
@@ -897,8 +986,8 @@ export class ProviderCatalog {
|
|
|
897
986
|
// ready plugin providers (TAL-288), under their own name, and providers keyed only through the credential pool (TAL-638).
|
|
898
987
|
const signedIn = new Set();
|
|
899
988
|
const pluginNames = new Map();
|
|
900
|
-
for (const row of
|
|
901
|
-
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'))
|
|
902
991
|
continue;
|
|
903
992
|
signedIn.add(str(row.id));
|
|
904
993
|
if (row.is_plugin_provider === true)
|
|
@@ -936,8 +1025,15 @@ export class ProviderCatalog {
|
|
|
936
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(/\/+$/, '')));
|
|
937
1026
|
detected.add(named?.[0] ?? active ?? 'custom');
|
|
938
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;
|
|
939
1033
|
const groups = [];
|
|
940
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;
|
|
941
1037
|
if (pid.startsWith('custom:')) {
|
|
942
1038
|
const g = namedCustom.get(pid);
|
|
943
1039
|
const models = [...(g?.models ?? [])];
|
|
@@ -945,6 +1041,11 @@ export class ProviderCatalog {
|
|
|
945
1041
|
groups.push({ provider: g?.name ?? pid.replace('custom:', ''), provider_id: pid, models: applyProviderPrefix(models, pid, active) });
|
|
946
1042
|
continue;
|
|
947
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
|
+
}
|
|
948
1049
|
if (pid === 'custom') {
|
|
949
1050
|
const models = [...customModels];
|
|
950
1051
|
for (const id of configuredIds.get(pid) ?? [])
|
|
@@ -956,20 +1057,25 @@ export class ProviderCatalog {
|
|
|
956
1057
|
}
|
|
957
1058
|
const providerCfg = dict(providersCfg[rawKeyFor.get(pid) ?? pid]);
|
|
958
1059
|
let raw = [];
|
|
1060
|
+
let allowFallback = false;
|
|
959
1061
|
if ('models' in providerCfg && providerCfg.models_discovered !== true)
|
|
960
1062
|
raw = configuredModelOptions(providerCfg.models);
|
|
961
|
-
if (!raw.length
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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)
|
|
965
1071
|
unconfirmed.add(pid);
|
|
966
1072
|
if (live.length)
|
|
967
1073
|
raw = live.map((id) => ({ id, label: pid === 'nous' ? `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` : labelForModel(id, []) }));
|
|
968
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.
|
|
969
|
-
else if (pid === 'nous' && !
|
|
1075
|
+
else if (keyed && pid === 'nous' && !failed)
|
|
970
1076
|
continue;
|
|
971
1077
|
}
|
|
972
|
-
if (!raw.length)
|
|
1078
|
+
if (!raw.length && allowFallback)
|
|
973
1079
|
raw = pid === 'openrouter' ? FALLBACK_MODELS.map((m) => ({ id: m.id, label: m.label })) : [...(PROVIDER_MODELS[pid] ?? [])];
|
|
974
1080
|
for (const id of configuredIds.get(pid) ?? [])
|
|
975
1081
|
if (!raw.some((m) => m.id === id))
|
|
@@ -977,7 +1083,7 @@ export class ProviderCatalog {
|
|
|
977
1083
|
if (raw.length)
|
|
978
1084
|
groups.push({ provider: pluginNames.get(pid) ?? displayName(pid), provider_id: pid, models: applyProviderPrefix(raw, pid, active) });
|
|
979
1085
|
}
|
|
980
|
-
if (
|
|
1086
|
+
if (pickerDefault) {
|
|
981
1087
|
const all = new Set(groups.flatMap((g) => g.models.map((m) => m.id)));
|
|
982
1088
|
if (!all.has(defaultModel) && !all.has(`@${active ?? ''}:${defaultModel}`)) {
|
|
983
1089
|
const label = labelForModel(defaultModel, groups);
|
|
@@ -1010,11 +1116,6 @@ export class ProviderCatalog {
|
|
|
1010
1116
|
}
|
|
1011
1117
|
const rank = (g) => (g.provider_id === active ? 0 : g.provider_id.startsWith('custom:') ? 1 : withKeys.has(g.provider_id) ? 2 : 3);
|
|
1012
1118
|
kept.sort((a, b) => rank(a) - rank(b) || (a.provider_id < b.provider_id ? -1 : a.provider_id > b.provider_id ? 1 : 0));
|
|
1013
|
-
for (const g of kept)
|
|
1014
|
-
if (isOpenAiFamilyProvider(g.provider_id))
|
|
1015
|
-
for (const m of g.models)
|
|
1016
|
-
if (/^(@[^:]+:)?(openai\/)?(gpt-5|o[0-9])/.test(m.id))
|
|
1017
|
-
m.supports_fast_tier = true;
|
|
1018
1119
|
const aliases = {};
|
|
1019
1120
|
const rawAliases = model.aliases;
|
|
1020
1121
|
if (isDict(rawAliases))
|
|
@@ -1022,19 +1123,118 @@ export class ProviderCatalog {
|
|
|
1022
1123
|
if (k && v)
|
|
1023
1124
|
aliases[k.trim()] = str(v).trim();
|
|
1024
1125
|
const [defaultBare, defaultProvider] = parseProviderQualifiedModel(defaultModel) ?? [defaultModel, active];
|
|
1025
|
-
|
|
1026
|
-
|
|
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) {
|
|
1027
1140
|
const providerId = active ?? 'default';
|
|
1028
|
-
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: {} });
|
|
1029
1142
|
}
|
|
1030
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) } : {}) }));
|
|
1031
|
-
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;
|
|
1032
1223
|
}
|
|
1033
1224
|
/** Stamps the default's entry id and keeps the catalog for `modelOptionFor`. */
|
|
1034
|
-
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
|
+
}
|
|
1035
1235
|
catalog.default_option_id = catalogOptionId(catalog, catalog.default_bare_id ?? null, catalog.default_provider_id ?? null);
|
|
1036
1236
|
this.lastModels.set(profileHome, catalog);
|
|
1037
|
-
this.lastModelsMeta.set(profileHome, {
|
|
1237
|
+
this.lastModelsMeta.set(profileHome, { source: meta.source, unconfirmed: meta.unconfirmed, at: this.deps.now() });
|
|
1038
1238
|
return structuredClone(catalog);
|
|
1039
1239
|
}
|
|
1040
1240
|
/** Python `_configured_model_badges_from_static_catalog`: which picker rows are the main/fallback selections. */
|
|
@@ -1066,14 +1266,17 @@ export class ProviderCatalog {
|
|
|
1066
1266
|
return badges;
|
|
1067
1267
|
}
|
|
1068
1268
|
/** Python `_handle_live_models`. */
|
|
1069
|
-
async liveModels(profileHome, providerRaw) {
|
|
1269
|
+
async liveModels(profileHome, providerRaw, attempt = 0) {
|
|
1270
|
+
const fingerprint = this.sourceFingerprint(profileHome);
|
|
1070
1271
|
const config = await this.deps.config.read(profileHome);
|
|
1071
1272
|
let provider = providerRaw.trim().toLowerCase() || str(modelSection(config).provider).trim().toLowerCase();
|
|
1072
1273
|
if (!provider)
|
|
1073
1274
|
return { error: 'no_provider', models: [] };
|
|
1074
1275
|
provider = resolveProviderAlias(provider);
|
|
1075
1276
|
let ids = await this.liveModelIds(profileHome, provider);
|
|
1076
|
-
|
|
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';
|
|
1077
1280
|
if (!ids.length) {
|
|
1078
1281
|
const fromConfig = [];
|
|
1079
1282
|
if (provider === 'custom' || provider.startsWith('custom:')) {
|
|
@@ -1083,16 +1286,33 @@ export class ProviderCatalog {
|
|
|
1083
1286
|
fromConfig.push(...[str(cp.model).trim(), ...configuredModelIds(cp.models)].filter(Boolean));
|
|
1084
1287
|
}
|
|
1085
1288
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
+
}
|
|
1088
1297
|
}
|
|
1089
|
-
// Python: the dropdown-enrichment surface keeps the picker's visibility budget (15 rows past 25) and annotates
|
|
1090
|
-
// OpenAI-family rows with `supports_fast_tier`; the answer carries `count`.
|
|
1091
1298
|
if (provider !== 'nous' && ids.length > MODEL_PICKER_OVERFLOW_THRESHOLD)
|
|
1092
1299
|
ids = ids.slice(0, MODEL_PICKER_VISIBLE_TARGET);
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
+
});
|
|
1096
1316
|
}
|
|
1097
1317
|
/**
|
|
1098
1318
|
* Python `_resolve_model_context_length` inputs: the effective base URL, key, and any configured window for this
|
|
@@ -1155,27 +1375,35 @@ export class ProviderCatalog {
|
|
|
1155
1375
|
if (envVar) {
|
|
1156
1376
|
for (const name of [envVar, ...(PROVIDER_ENV_VAR_ALIASES[pid] ?? [])]) {
|
|
1157
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.
|
|
1158
1379
|
if (v)
|
|
1159
|
-
return v;
|
|
1380
|
+
return v.startsWith('op://') ? null : v;
|
|
1160
1381
|
}
|
|
1161
1382
|
}
|
|
1162
1383
|
// The same config sources `providerHasKey` counts, so a provider shown as keyed resolves its key.
|
|
1163
1384
|
const model = modelSection(config);
|
|
1164
1385
|
if (canonicaliseProviderId(model.provider) === canonicaliseProviderId(pid) && valueCountsAsApiKey(pid, model.api_key))
|
|
1165
|
-
return str(model.api_key).trim();
|
|
1386
|
+
return str(model.api_key).trim().startsWith('op://') ? null : str(model.api_key).trim();
|
|
1166
1387
|
for (const [key, value] of Object.entries(dict(config.providers))) {
|
|
1167
1388
|
if (providerIdentity(key) === providerIdentity(pid) && isDict(value) && valueCountsAsApiKey(pid, value.api_key))
|
|
1168
|
-
return str(value.api_key).trim();
|
|
1389
|
+
return str(value.api_key).trim().startsWith('op://') ? null : str(value.api_key).trim();
|
|
1169
1390
|
}
|
|
1170
1391
|
return null;
|
|
1171
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
|
+
}
|
|
1172
1398
|
/** Account-usage snapshots by profile home, provider, and pool account. */
|
|
1173
1399
|
accountUsageCache = new Map();
|
|
1400
|
+
quotaReadSequence = 0;
|
|
1174
1401
|
/**
|
|
1175
1402
|
* Python `get_provider_quota`. `credential` scopes the read to one credential-pool account (TAL-548): a benched account
|
|
1176
1403
|
* answers with its local pool state, and every other read uses that account's key.
|
|
1177
1404
|
*/
|
|
1178
1405
|
async quota(profileHome, providerRaw, opts = {}) {
|
|
1406
|
+
const sequence = opts.sequence ?? ++this.quotaReadSequence;
|
|
1179
1407
|
const at = opts.at ?? this.deps.now();
|
|
1180
1408
|
const computed_at = isoAt(at);
|
|
1181
1409
|
const config = await this.deps.config.read(profileHome);
|
|
@@ -1193,7 +1421,9 @@ export class ProviderCatalog {
|
|
|
1193
1421
|
let limits = null;
|
|
1194
1422
|
// Python `_ACCOUNT_USAGE_CACHE_TTL_SECONDS`: a snapshot answers repeat polls for 45 s unless `refresh` is set.
|
|
1195
1423
|
const cacheKey = `${profileHome}\0${provider}\0${credential?.credential_id ?? ''}`;
|
|
1196
|
-
const
|
|
1424
|
+
const hit = this.accountUsageCache.get(cacheKey);
|
|
1425
|
+
const source = this.sourceFingerprint(profileHome);
|
|
1426
|
+
const cached = hit?.source === source ? hit : undefined;
|
|
1197
1427
|
if (cached && !opts.refresh && this.deps.now() - cached.at <= ACCOUNT_USAGE_CACHE_TTL_S)
|
|
1198
1428
|
limits = cached.limits;
|
|
1199
1429
|
else if (sidecar) {
|
|
@@ -1206,7 +1436,12 @@ export class ProviderCatalog {
|
|
|
1206
1436
|
catch (error) {
|
|
1207
1437
|
limits = { available: false, unavailable_reason: str(error.message), windows: [], details: [] };
|
|
1208
1438
|
}
|
|
1209
|
-
|
|
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;
|
|
1210
1445
|
}
|
|
1211
1446
|
// The cache keeps the Agent's raw windows; pace is recomputed as of every response.
|
|
1212
1447
|
if (limits)
|
|
@@ -1216,11 +1451,11 @@ export class ProviderCatalog {
|
|
|
1216
1451
|
const reason = str(limits?.unavailable_reason).trim();
|
|
1217
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 });
|
|
1218
1453
|
}
|
|
1219
|
-
if (provider
|
|
1454
|
+
if (isBalanceProvider(provider)) {
|
|
1220
1455
|
const apiKey = this.apiKeyFor(provider, profileHome, config);
|
|
1221
|
-
const key = credential ? { credential_id: credential.credential_id } :
|
|
1222
|
-
const read =
|
|
1223
|
-
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;
|
|
1224
1459
|
if (provider === 'openrouter') {
|
|
1225
1460
|
if (status === 'no_key')
|
|
1226
1461
|
return answer(false, 'no_key', 'OpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.');
|
|
@@ -1254,7 +1489,7 @@ export class ProviderCatalog {
|
|
|
1254
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 });
|
|
1255
1490
|
}
|
|
1256
1491
|
// Python `_local_pool_snapshot`: any other provider with a credential pool shows its accounts' local state.
|
|
1257
|
-
const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)) ?? [];
|
|
1492
|
+
const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)).value?.entries ?? [];
|
|
1258
1493
|
if (entries.length) {
|
|
1259
1494
|
const limits = credential ? poolEntryLimits(credential, at) : poolLimits(entries, at);
|
|
1260
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 });
|
|
@@ -1266,7 +1501,7 @@ export class ProviderCatalog {
|
|
|
1266
1501
|
* `apiKey`, the provider's configured key. Null when the pool is unknown (see `poolLookup`).
|
|
1267
1502
|
*/
|
|
1268
1503
|
poolEntries(profileHome, pid, apiKey) {
|
|
1269
|
-
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) }));
|
|
1270
1505
|
}
|
|
1271
1506
|
/** The last answer of each credential-pool lookup, by profile home and lookup. */
|
|
1272
1507
|
lastPoolAnswers = new Map();
|
|
@@ -1275,23 +1510,48 @@ export class ProviderCatalog {
|
|
|
1275
1510
|
* lookup, else null: an unknown pool, never an empty one, so no pool account is ever reported as removed.
|
|
1276
1511
|
*/
|
|
1277
1512
|
async poolLookup(profileHome, lookup, call) {
|
|
1513
|
+
const source = this.sourceFingerprint(profileHome);
|
|
1278
1514
|
const key = `${profileHome}\0${lookup}`;
|
|
1279
1515
|
try {
|
|
1280
1516
|
const sidecar = this.deps.sidecar();
|
|
1281
1517
|
if (!sidecar)
|
|
1282
1518
|
throw new Error('the sidecar is not running');
|
|
1283
1519
|
const answer = await call(sidecar);
|
|
1284
|
-
this.
|
|
1285
|
-
|
|
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 };
|
|
1286
1524
|
}
|
|
1287
1525
|
catch (error) {
|
|
1288
1526
|
this.deps.log(`[catalog] credential pool lookup ${lookup.replace('\0', ' ')} failed: ${str(error.message)}`);
|
|
1289
|
-
|
|
1527
|
+
const hit = this.lastPoolAnswers.get(key);
|
|
1528
|
+
return { value: hit?.source === source && source === this.sourceFingerprint(profileHome) ? hit.value : null, authoritative: false };
|
|
1290
1529
|
}
|
|
1291
1530
|
}
|
|
1292
|
-
/**
|
|
1531
|
+
/** One shared read of pool membership and scoped key revisions before reusing catalog data. */
|
|
1293
1532
|
poolProviders(profileHome) {
|
|
1294
|
-
|
|
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;
|
|
1295
1555
|
}
|
|
1296
1556
|
/** TAL-548: one key-based balance read through the sidecar, which resolves a pool account's key itself; null when it fails. */
|
|
1297
1557
|
async balance(profileHome, provider, key) {
|
|
@@ -1308,52 +1568,46 @@ export class ProviderCatalog {
|
|
|
1308
1568
|
}
|
|
1309
1569
|
quotaScopeCache = null;
|
|
1310
1570
|
/** Python `_quota_server_scope_id`: a 32-hex id persisted at `<state>/.quota_scope_id` (created atomically once). */
|
|
1311
|
-
quotaServerScopeId() {
|
|
1571
|
+
async quotaServerScopeId() {
|
|
1312
1572
|
if (this.quotaScopeCache)
|
|
1313
1573
|
return this.quotaScopeCache;
|
|
1314
|
-
const
|
|
1315
|
-
if (!
|
|
1574
|
+
const documents = this.deps.documents;
|
|
1575
|
+
if (!documents) {
|
|
1316
1576
|
this.quotaScopeCache = randomUUID().replaceAll('-', '');
|
|
1317
1577
|
return this.quotaScopeCache;
|
|
1318
1578
|
}
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
if (!/^[0-9a-f]{32}$/.test(stored)) {
|
|
1328
|
-
stored = randomUUID().replaceAll('-', '');
|
|
1329
|
-
mkdirSync(dir, { recursive: true });
|
|
1330
|
-
const temporary = `${path}.tmp.${String(process.pid)}`;
|
|
1331
|
-
writeFileSync(temporary, `${stored}\n`, 'utf8');
|
|
1332
|
-
renameSync(temporary, path);
|
|
1333
|
-
}
|
|
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
|
+
});
|
|
1334
1586
|
this.quotaScopeCache = stored;
|
|
1335
1587
|
return stored;
|
|
1336
1588
|
}
|
|
1337
1589
|
/** Python `_quota_profile_scope_id`. */
|
|
1338
|
-
quotaProfileScopeId(profile) {
|
|
1339
|
-
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)}`;
|
|
1340
1592
|
}
|
|
1341
1593
|
/**
|
|
1342
1594
|
* Python `get_provider_quotas`: the stable-identity envelope the iOS widget persists. A keyed provider is one source per
|
|
1343
1595
|
* credential-pool account, else one for its own key (Python `_quota_source_descriptors`, TAL-548). Each source id appears
|
|
1344
1596
|
* once (two custom providers can share a slug), ordered by provider, account label, then source id.
|
|
1345
1597
|
*/
|
|
1346
|
-
async quotas(profileHome, profile, opts = {}) {
|
|
1598
|
+
async quotas(profileHome, profile, opts = {}, attempt = 0) {
|
|
1599
|
+
const sequence = ++this.quotaReadSequence;
|
|
1600
|
+
const source = this.sourceFingerprint(profileHome);
|
|
1347
1601
|
const at = this.deps.now();
|
|
1348
1602
|
const { payload: status, pooled: pooledIds } = await this.providerRows(profileHome);
|
|
1349
1603
|
const active = status.active_provider;
|
|
1350
|
-
const scopeId = this.quotaProfileScopeId(profile);
|
|
1604
|
+
const scopeId = (await this.quotaProfileScopeId(profile));
|
|
1351
1605
|
// Python `_quota_source_id(profile, provider, credential_id)`: a pool account's id, or "provider" for the provider's own key.
|
|
1352
1606
|
const sourceId = (pid, credential) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\0${credential}`).digest('hex').slice(0, 32)}`;
|
|
1353
1607
|
// A provider configured only through `hermes auth add` has no key in .env or config.yaml, yet its pool accounts are
|
|
1354
1608
|
// sources, including a signed-out OAuth provider's. An unknown pool (a failed lookup with no earlier answer) never
|
|
1355
1609
|
// reports a requested source as missing.
|
|
1356
|
-
let poolUnknown =
|
|
1610
|
+
let poolUnknown = !status.availability.authoritative;
|
|
1357
1611
|
const pooled = new Set(pooledIds ?? []);
|
|
1358
1612
|
const config = await this.deps.config.read(profileHome);
|
|
1359
1613
|
const perProvider = await Promise.all(status.providers.filter((p) => p.has_key || p.is_custom || pooled.has(str(p.id))).map(async (p) => {
|
|
@@ -1361,30 +1615,80 @@ export class ProviderCatalog {
|
|
|
1361
1615
|
const label = str(p.display_name) || pid;
|
|
1362
1616
|
const apiKey = this.apiKeyFor(pid, profileHome, config);
|
|
1363
1617
|
const found = await this.poolEntries(profileHome, pid, apiKey);
|
|
1364
|
-
if (!found)
|
|
1618
|
+
if (!found.authoritative)
|
|
1365
1619
|
poolUnknown = true;
|
|
1366
|
-
const entries = found ?? [];
|
|
1620
|
+
const entries = found.value?.entries ?? [];
|
|
1367
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 }));
|
|
1368
1622
|
// The provider's own key is a source unless a pool account already holds it.
|
|
1369
|
-
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)));
|
|
1370
1624
|
return ownKey ? [{ source_id: sourceId(pid, 'provider'), provider_id: pid, provider_label: label, account_label: label, credential: null }, ...accounts] : accounts;
|
|
1371
1625
|
}));
|
|
1372
1626
|
let descriptors = uniqueQuotaSources(perProvider.flat());
|
|
1627
|
+
const membership = new Set(descriptors.map((d) => d.source_id));
|
|
1373
1628
|
const requested = str(opts.sourceId).trim() || null;
|
|
1374
1629
|
if (requested)
|
|
1375
1630
|
descriptors = descriptors.filter((d) => d.source_id === requested);
|
|
1376
|
-
const thresholds = this.deps.quotaThresholds?.(profile) ?? QUOTA_THRESHOLD_DEFAULTS;
|
|
1377
|
-
|
|
1378
|
-
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 });
|
|
1379
1634
|
const limits = dict(q.account_limits);
|
|
1380
1635
|
return classifyQuotaSource({
|
|
1381
|
-
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,
|
|
1382
1637
|
is_active_provider: d.provider_id === active, supported: q.supported === true, status: str(limits.status) || str(q.status) || 'unavailable',
|
|
1383
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 ?? [],
|
|
1384
1639
|
unavailable_reason: limits.unavailable_reason ?? null, retry_after: limits.retry_after ?? null, fetched_at: limits.fetched_at ?? null, message: q.message ?? null,
|
|
1385
1640
|
}, thresholds);
|
|
1386
1641
|
}));
|
|
1387
|
-
|
|
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') };
|
|
1388
1692
|
}
|
|
1389
1693
|
/** Python `get_provider_cost_history` (OpenRouter only; daily snapshots under `<home>/cost-snapshots`). */
|
|
1390
1694
|
async costHistory(profileHome, providerRaw, days) {
|
|
@@ -1397,9 +1701,9 @@ export class ProviderCatalog {
|
|
|
1397
1701
|
const budget = this.deps.costBudget();
|
|
1398
1702
|
const config = await this.deps.config.read(profileHome);
|
|
1399
1703
|
const apiKey = this.apiKeyFor('openrouter', profileHome, config);
|
|
1400
|
-
|
|
1704
|
+
const info = await this.balance(profileHome, 'openrouter', this.usageKey('openrouter', apiKey));
|
|
1705
|
+
if (info?.status === 'no_key')
|
|
1401
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.' };
|
|
1402
|
-
const info = await this.balance(profileHome, 'openrouter', { api_key: apiKey });
|
|
1403
1707
|
if (info?.status !== 'ok' || !info.quota) {
|
|
1404
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.' };
|
|
1405
1709
|
}
|