@maudecode/talaria-web 1.16.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +8 -8
- package/dist/api/auth-raw.d.ts +1 -1
- package/dist/api/auth-raw.js +4 -4
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +6 -0
- package/dist/api/automation-raw.js +59 -9
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +163 -10
- package/dist/api/automation-router.js +25 -8
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +700 -15
- package/dist/api/chat-router.js +74 -55
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +47 -2
- package/dist/api/git-router.js +31 -22
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +14 -0
- package/dist/api/raw-routes.js +277 -36
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +10 -0
- package/dist/api/router.js +10 -8
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +1710 -73
- package/dist/api/sessions-router.js +164 -10
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +132 -2
- package/dist/api/settings-router.js +15 -5
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +16 -3
- package/dist/api/sse-routes.js +194 -67
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.js +12 -25
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +425 -19
- package/dist/api/tools-router.js +157 -26
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +0 -9377
- package/dist/app.js +42 -17
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +8 -2
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +5 -2
- package/dist/auth/oidc.js +8 -7
- package/dist/auth/oidc.js.map +1 -1
- package/dist/bin/talaria-web.js +1 -1
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/dotenv.js +1 -0
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/config/agent-config.d.ts +2 -0
- package/dist/config/agent-config.js +12 -2
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/http/context.d.ts +20 -3
- package/dist/http/context.js +41 -8
- package/dist/http/context.js.map +1 -1
- package/dist/http/csp.d.ts +2 -1
- package/dist/http/csp.js +3 -2
- package/dist/http/csp.js.map +1 -1
- package/dist/onboarding.js +4 -0
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +10 -0
- package/dist/profiles/profiles.js +40 -2
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/providers/catalog.d.ts +101 -8
- package/dist/providers/catalog.js +463 -123
- package/dist/providers/catalog.js.map +1 -1
- package/dist/raster-image.d.ts +7 -0
- package/dist/raster-image.js +267 -0
- package/dist/raster-image.js.map +1 -0
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +354 -44
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +111 -27
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +2 -0
- package/dist/sessions/anchor.js +8 -3
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +2 -9
- package/dist/sessions/background-tasks.js +2 -12
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/claude-code.d.ts +19 -0
- package/dist/sessions/claude-code.js +250 -0
- package/dist/sessions/claude-code.js.map +1 -0
- package/dist/sessions/cli-sessions.d.ts +15 -4
- package/dist/sessions/cli-sessions.js +120 -29
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +61 -9
- package/dist/sessions/completions.js +282 -34
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +14 -1
- package/dist/sessions/compress.js +52 -11
- package/dist/sessions/compress.js.map +1 -1
- package/dist/sessions/export.d.ts +5 -0
- package/dist/sessions/export.js +30 -0
- package/dist/sessions/export.js.map +1 -1
- package/dist/sessions/gateway-watcher.d.ts +7 -3
- package/dist/sessions/gateway-watcher.js +31 -19
- package/dist/sessions/gateway-watcher.js.map +1 -1
- package/dist/sessions/handoff.d.ts +32 -0
- package/dist/sessions/handoff.js +114 -0
- package/dist/sessions/handoff.js.map +1 -0
- package/dist/sessions/journal.d.ts +8 -0
- package/dist/sessions/journal.js +44 -10
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +13 -3
- package/dist/sessions/list.js +46 -15
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +53 -5
- package/dist/sessions/merge.js +219 -23
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/recovery.d.ts +22 -0
- package/dist/sessions/recovery.js +506 -0
- package/dist/sessions/recovery.js.map +1 -0
- package/dist/sessions/relay.d.ts +21 -2
- package/dist/sessions/relay.js +156 -11
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +152 -8
- package/dist/sessions/service.js +640 -72
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +17 -1
- package/dist/sessions/session.js +63 -4
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/state-db-worker.d.ts +1 -0
- package/dist/sessions/state-db-worker.js +33 -0
- package/dist/sessions/state-db-worker.js.map +1 -0
- package/dist/sessions/state-db.d.ts +87 -4
- package/dist/sessions/state-db.js +417 -8
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/store.d.ts +7 -0
- package/dist/sessions/store.js +36 -22
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +10 -0
- package/dist/sessions/streams.js +14 -3
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.d.ts +2 -0
- package/dist/sessions/titles.js +4 -0
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/todo.d.ts +6 -1
- package/dist/sessions/todo.js +11 -4
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +12 -1
- package/dist/sessions/tool-display.js +50 -1
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +1 -1
- package/dist/sessions/turn-context.js +11 -7
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +67 -11
- package/dist/sessions/turn.js +413 -111
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +59 -0
- package/dist/sessions/wakeup-pause.js +181 -0
- package/dist/sessions/wakeup-pause.js.map +1 -0
- package/dist/settings.d.ts +20 -2
- package/dist/settings.js +45 -9
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/fake.d.ts +4 -4
- package/dist/sidecar/fake.js +5 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/spa.js +2 -1
- package/dist/spa.js.map +1 -1
- package/dist/startup.d.ts +13 -5
- package/dist/startup.js +48 -0
- package/dist/startup.js.map +1 -1
- package/dist/test/harness.js +2 -0
- package/dist/test/harness.js.map +1 -1
- package/dist/test/ts-worker-hooks.d.ts +1 -0
- package/dist/test/ts-worker-hooks.js +24 -0
- package/dist/test/ts-worker-hooks.js.map +1 -0
- package/dist/text/diff.d.ts +9 -14
- package/dist/text/diff.js +74 -108
- package/dist/text/diff.js.map +1 -1
- package/dist/tools/crons.js +45 -8
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +53 -0
- package/dist/tools/dashboard-plugins.js +188 -0
- package/dist/tools/dashboard-plugins.js.map +1 -0
- package/dist/tools/extensions.d.ts +13 -1
- package/dist/tools/extensions.js +132 -60
- package/dist/tools/extensions.js.map +1 -1
- package/dist/tools/health.d.ts +32 -0
- package/dist/tools/health.js +111 -19
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/insights.d.ts +8 -2
- package/dist/tools/insights.js +18 -2
- package/dist/tools/insights.js.map +1 -1
- package/dist/tools/kanban.d.ts +41 -1
- package/dist/tools/kanban.js +113 -2
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/mcp.d.ts +15 -1
- package/dist/tools/mcp.js +99 -5
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/project-os.d.ts +11 -0
- package/dist/tools/project-os.js +267 -0
- package/dist/tools/project-os.js.map +1 -0
- package/dist/tools/skills.d.ts +5 -1
- package/dist/tools/skills.js +5 -3
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/terminal.d.ts +1 -0
- package/dist/tools/terminal.js +1 -1
- package/dist/tools/terminal.js.map +1 -1
- package/dist/tools/updates.d.ts +33 -8
- package/dist/tools/updates.js +236 -57
- package/dist/tools/updates.js.map +1 -1
- package/dist/tools/wiki.d.ts +68 -0
- package/dist/tools/wiki.js +413 -0
- package/dist/tools/wiki.js.map +1 -0
- package/dist/workspace/escape.d.ts +61 -0
- package/dist/workspace/escape.js +149 -0
- package/dist/workspace/escape.js.map +1 -0
- package/dist/workspace/extract.d.ts +22 -0
- package/dist/workspace/extract.js +308 -0
- package/dist/workspace/extract.js.map +1 -0
- package/dist/workspace/fs.d.ts +19 -3
- package/dist/workspace/fs.js +96 -23
- package/dist/workspace/fs.js.map +1 -1
- package/dist/workspace/git.d.ts +18 -0
- package/dist/workspace/git.js +38 -25
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +4 -4
- package/dist/workspace/media.js +18 -45
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/preview.d.ts +28 -0
- package/dist/workspace/preview.js +110 -0
- package/dist/workspace/preview.js.map +1 -0
- package/dist/workspace/rollback.d.ts +36 -21
- package/dist/workspace/rollback.js +179 -270
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/unzip.js +4 -1
- package/dist/workspace/unzip.js.map +1 -1
- package/dist/workspace/upload.d.ts +9 -4
- package/dist/workspace/upload.js +9 -3
- package/dist/workspace/upload.js.map +1 -1
- package/dist/workspace/worktrees.d.ts +2 -1
- package/dist/workspace/worktrees.js +14 -15
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +4 -2
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/scripts/replay_sidecar.py +25 -1
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/__main__.py +7 -1
- package/sidecar/talaria_sidecar/home.py +52 -2
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +39 -5
- package/sidecar/talaria_sidecar/methods/chat.py +431 -43
- package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
- package/sidecar/talaria_sidecar/methods/commands.py +96 -2
- package/sidecar/talaria_sidecar/methods/cron.py +39 -8
- package/sidecar/talaria_sidecar/methods/gateway.py +32 -15
- package/sidecar/talaria_sidecar/methods/kanban.py +140 -19
- package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
- package/sidecar/talaria_sidecar/methods/process.py +115 -2
- package/sidecar/talaria_sidecar/methods/profiles.py +84 -8
- package/sidecar/talaria_sidecar/methods/providers.py +27 -8
- package/sidecar/talaria_sidecar/methods/runtime.py +3 -0
- package/sidecar/talaria_sidecar/methods/state_db.py +39 -5
- package/sidecar/talaria_sidecar/methods/text.py +17 -1
- package/sidecar/talaria_sidecar/methods/usage.py +257 -4
- package/sidecar/talaria_sidecar/runtime.py +17 -1
- package/static/dist/FILES.txt +127 -125
- package/static/dist/assets/AppShell-g3bkgnvt.js +1 -0
- package/static/dist/assets/ChatPage-oo7bdan6.js +25 -0
- package/static/dist/assets/Dialog-g355qxhp.js +1 -0
- package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +1 -0
- package/static/dist/assets/HubRoute-lmkrbw47.js +2 -0
- package/static/dist/assets/{Markdown-jqrx5es0.js → Markdown-j4kut4j2.js} +125 -125
- package/static/dist/assets/RightPanel-ohlsdzfl.js +1 -0
- package/static/dist/assets/Select-kddq75qc.js +1 -0
- package/static/dist/assets/SessionListPanel-lu3oy0hw.js +1 -0
- package/static/dist/assets/SettingsLayout-o9bv4mgb.js +1 -0
- package/static/dist/assets/States-eq6fe29m.js +1 -0
- package/static/dist/assets/Toaster-pc247u9f.js +1 -0
- package/static/dist/assets/_app-f8n1i0m1.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +1 -0
- package/static/dist/assets/_app.index-bzzv80hk.js +1 -0
- package/static/dist/assets/_app.insights-dzixbryi.js +1 -0
- package/static/dist/assets/_app.kanban-c6hep2us.js +1 -0
- package/static/dist/assets/_app.logs-i69qds98.js +1 -0
- package/static/dist/assets/_app.memory-kl4vf0gd.js +1 -0
- package/static/dist/assets/_app.profiles-b8zinmyp.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-hh27wwu2.js → _app.session._sessionId-fn1bhde6.js} +1 -1
- package/static/dist/assets/_app.settings-cg8plwmf.js +1 -0
- package/static/dist/assets/_app.settings._section-bkwbi9zn.js +3 -0
- package/static/dist/assets/_app.skills-cs47sbvs.js +1 -0
- package/static/dist/assets/_app.tasks-dwvk64tc.js +1 -0
- package/static/dist/assets/_app.todos-c8xjm8p3.js +1 -0
- package/static/dist/assets/_app.workspaces-ehvd1gds.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-oity9y4o.js → abnfDiagram-VCTEODGH-c8kdixke.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-ceaepnyr.js → architectureDiagram-5GKGNRK7-cku06x1p.js} +1 -1
- package/static/dist/assets/{back-eu64w89w.js → back-h7aun7ew.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-n79st4pj.js → blockDiagram-I7D4REHJ-jzthp1wa.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mi73akzk.js → c4Diagram-7LVT6UL2-mwr67mt8.js} +1 -1
- package/static/dist/assets/{cancel-mzvkdjg8.js → cancel-fhi0vcnu.js} +1 -1
- package/static/dist/assets/channel-b2zo73z1.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-laat0g1q.js → chunk-4HAMMTFA-gpq1y228.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-0psy1tr2.js → chunk-75Z2AOVW-ktje58c4.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lw8uox8x.js → chunk-DU6HZSFF-ofajdixh.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-erassib3.js → chunk-F27PBJKO-cmev85ti.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-fz7rx619.js → chunk-GMAD6QVW-htjgt22y.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-gdb0epb2.js → chunk-GVQU2GXP-qndvoxl7.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-ea8t26qw.js → chunk-IMKFNOWR-e5bkd5q6.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-mb1gu9kz.js → chunk-L3NEJ4N5-mj1155r6.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-eovmccvy.js → chunk-OSK3NFVY-e7gkikcw.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-bo55zjo5.js → chunk-P2QGCYS3-f7t54xfm.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-ja7ohe4h.js → chunk-POPQ4Y6H-i927v9oa.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-e30g9nus.js → chunk-PWAF6VOD-m84lmnyt.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-gbb798f7.js → chunk-SHT3W25Y-mi0tlcu9.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-h2iw8urm.js → chunk-SVP7TREG-dnau702j.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-fd2cottz.js → chunk-TICWLB2K-edem1gkg.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +1 -0
- package/static/dist/assets/{copied-h65fc3cx.js → copied-hcplv46p.js} +1 -1
- package/static/dist/assets/copy-magtofyp.js +1 -0
- package/static/dist/assets/{cynefin-OW5HDTMX-lbltyl5j.js → cynefin-OW5HDTMX-siqlulkl.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-lxhsqq4a.js → cynefinDiagram-5FMLGOSQ-i648tah9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-gn4nnaln.js → dagre-GXQ25YYZ-mwn6uc17.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-pa3ij6q7.js → diagram-S7CK7UJ4-l8abxaes.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-zbipyz57.js → diagram-UQ7AKVKN-mekq7zgq.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-d1kbpzgf.js → diagram-VSXAHHWV-fnky6lvc.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-i3ty14xl.js → diagram-VX7I27RA-xe6r5qfw.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-m8xefqa7.js → diagram-Z3DM3KII-nq07dg7k.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-nz7wj05l.js → ebnfDiagram-PWID7BFC-c62ipm3g.js} +1 -1
- package/static/dist/assets/endpoints-klbxod4t.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fa41ib83.js → erDiagram-RLTQ6QDP-h39kmnzy.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-n7zq01qu.js → ganttDiagram-EL5Y4UJY-ngbsyp37.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-c89emb07.js → gitGraph-4MIJSDKK-fr2i7caa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-cz7cgr6w.js → gitGraphDiagram-WWUBYQGX-eravdti4.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-d9bapmni.js → highlighted-body-KPVGNVTW-jwpop9p8.js} +1 -1
- package/static/dist/assets/host-i828xuo7.js +1 -0
- package/static/dist/assets/index-eh8zilnv.css +2 -0
- package/static/dist/assets/index-r648f4o1.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-ndddhhhh.js → info-A6RAGUB7-h11x0dxo.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-ccxigcpf.js → infoDiagram-27XIBGKW-dfh47dw9.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-le0z9vtc.js → ishikawaDiagram-5VMMS53U-i6ypq351.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-ivsmb26o.js → journeyDiagram-3NMN7TZE-lduf1z95.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-h6n2awt0.js → kanban-definition-UXKFOSKX-e18bu4pg.js} +1 -1
- package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +1 -0
- package/static/dist/assets/{line-dmzlfe0u.js → line-meizqsvn.js} +1 -1
- package/static/dist/assets/link-oot2yn73.js +1 -0
- package/static/dist/assets/login-gu28l9ou.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-gwyd5bnq.js → mermaid-parser.core-ntne45i9.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-l68gg5h7.js → mindmap-definition-YA3MSWOX-27b26eok.js} +1 -1
- package/static/dist/assets/onboarding-lkphzu1s.js +1 -0
- package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-gpv06065.js → packet-AYTQ26CC-l6jdka8p.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-e52vs23u.js → pegDiagram-XKGWAZYB-g1er3bxj.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-l6m7nz5s.js → pie-WAS4IAKB-i0ylcrt9.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-b4is36bq.js → pieDiagram-E7YTZNPT-fbz1e55z.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-m8vt2788.js → quadrantDiagram-AXDQQJYC-hhue7d8o.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-nxbrhiov.js → radar-RG4KPBEZ-f9b70aa4.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-222mu9hi.js → railroad-74A4TZTK-lgsuoeda.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-o8lldfzv.js → railroadDiagram-O6MQD6OU-eqpy9fnz.js} +1 -1
- package/static/dist/assets/refresh-abtu93ke.js +1 -0
- package/static/dist/assets/refresh-cw-hlwb02qv.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ovyq38kv.js → requirementDiagram-BXWQKSXE-ogf6puhi.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-ou7lznor.js → sankeyDiagram-P5KCCOFB-fqb8xcjf.js} +1 -1
- package/static/dist/assets/{saved-bazmcup9.js → saved-girnabkf.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-ovckdzf2.js → sequenceDiagram-WJ2MYXX4-dlcz6qjz.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-ertwwhlx.js → settings_section_extensions_title-h14cwk6f.js} +1 -1
- package/static/dist/assets/share._token-kl0r7ido.js +2 -0
- package/static/dist/assets/sliders-horizontal-n1zm34qq.js +1 -0
- package/static/dist/assets/sse-iwku0sjo.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-fcrexkkv.js → stateDiagram-D77RDMKH-fyoznpcs.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-f7usu8fw.js → swimlanes-42K2YHIH-oefldhfy.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-gp1hvz9f.js → timeline-definition-24CTP7MA-b1vl2zil.js} +1 -1
- package/static/dist/assets/todoStore-jjr5yr78.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-gb9gf11u.js → treeView-Q6P3EWNA-f7hcci5o.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-nuvfnvvi.js → treemap-WGGIJYW6-nl85071a.js} +1 -1
- package/static/dist/assets/units-kgxzxfhi.js +1 -0
- package/static/dist/assets/{untitled-htelhc68.js → untitled-cikfjntn.js} +1 -1
- package/static/dist/assets/{useForm-h2t0u14e.js → useForm-kqoqg0k9.js} +1 -1
- package/static/dist/assets/useLocale-o59tbiea.js +1 -0
- package/static/dist/assets/{useParams-dqd64jv7.js → useParams-b4dq6n4q.js} +1 -1
- package/static/dist/assets/{useSearch-j30pvsy7.js → useSearch-ipfmv3he.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-fzchbzaj.js → vennDiagram-4TSXK5OY-h5hzbk0n.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-m47mhfbh.js → wardley-WFR3VGLG-jftvpm3e.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-odg6ct1z.js → wardleyDiagram-VM6X3IG4-hedh85ge.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mrhms5um.js → xychartDiagram-S5SC5T6Z-e49523ab.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +2 -2
- package/static/dist/assets/AppShell-ef3bhu32.js +0 -1
- package/static/dist/assets/Button-nx37f4u1.js +0 -1
- package/static/dist/assets/ChatPage-cdcpykcg.js +0 -25
- package/static/dist/assets/Dialog-mec42hvb.js +0 -1
- package/static/dist/assets/HubRoute-ecb65zpr.js +0 -2
- package/static/dist/assets/RightPanel-d1lgvk4s.js +0 -1
- package/static/dist/assets/Select-h4ivl9qf.js +0 -1
- package/static/dist/assets/SessionListPanel-ewib046b.js +0 -1
- package/static/dist/assets/SettingsLayout-lx654cup.js +0 -1
- package/static/dist/assets/States-ehsb6lzo.js +0 -1
- package/static/dist/assets/Toaster-mch1pa3t.js +0 -1
- package/static/dist/assets/_app-m8d961ef.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +0 -1
- package/static/dist/assets/_app.index-dk6zp7e0.js +0 -1
- package/static/dist/assets/_app.insights-jt2fk4i1.js +0 -1
- package/static/dist/assets/_app.kanban-if4pubeu.js +0 -1
- package/static/dist/assets/_app.logs-mfaw9m6g.js +0 -1
- package/static/dist/assets/_app.memory-h3eaarb6.js +0 -1
- package/static/dist/assets/_app.profiles-ienktovs.js +0 -1
- package/static/dist/assets/_app.settings-dpoe0bzf.js +0 -1
- package/static/dist/assets/_app.settings._section-l468ynag.js +0 -3
- package/static/dist/assets/_app.skills-kcbncc8i.js +0 -1
- package/static/dist/assets/_app.tasks-occdrqhw.js +0 -2
- package/static/dist/assets/_app.todos-nnke2rcc.js +0 -1
- package/static/dist/assets/_app.workspaces-cdlatvwj.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +0 -1
- package/static/dist/assets/channel-mbvmf2vv.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +0 -1
- package/static/dist/assets/copy-e6977uwq.js +0 -1
- package/static/dist/assets/endpoints-k7yf25m0.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +0 -1
- package/static/dist/assets/index-h00usru2.js +0 -11
- package/static/dist/assets/index-n73ijkv2.css +0 -2
- package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +0 -1
- package/static/dist/assets/link-budeg5t9.js +0 -1
- package/static/dist/assets/login-kjhasvp8.js +0 -1
- package/static/dist/assets/matchContext-f8oy3lo7.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +0 -1
- package/static/dist/assets/onboarding-cdfrk0pj.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +0 -1
- package/static/dist/assets/refresh-cw-kbcje1uy.js +0 -1
- package/static/dist/assets/search-jen74z4u.js +0 -1
- package/static/dist/assets/share._token-dan9hgvj.js +0 -2
- package/static/dist/assets/sliders-horizontal-f9cb3lh3.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +0 -8
- package/static/dist/assets/todoStore-eqtid4r4.js +0 -1
- package/static/dist/assets/useLocale-hcsb84pj.js +0 -1
- package/static/dist/assets/useNavigate-nwy70ztj.js +0 -1
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* here. ponytail: one live-id cache with a 24h TTL replaces the Python
|
|
9
9
|
* publisher/provenance machinery; `refresh()` evicts it.
|
|
10
10
|
*/
|
|
11
|
-
import { readCapped } from '../http/capped.js';
|
|
12
11
|
import { homeDotenvKeys } from '../cli/dotenv.js';
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
14
13
|
import { createHash, randomUUID } from 'node:crypto';
|
|
15
14
|
import { join } from 'node:path';
|
|
16
15
|
import { atomicWriteText } from '../fs/atomic.js';
|
|
17
16
|
import { str } from '../util.js';
|
|
17
|
+
import { QUOTA_THRESHOLD_DEFAULTS } from '../settings.js';
|
|
18
18
|
import { loadEnvFile } from './env-file.js';
|
|
19
19
|
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
20
|
import { activeProviderFromConfig, canonicaliseProviderId, configuredModelIds, configuredModelOptions, customProviderEntries, customProviderSlug, dict, effectiveDefaultModel, isDict, isOpenAiFamilyProvider, mainModelSupportsServiceTier, modelSection, parseProviderQualifiedModel, providerIdentity, resolveProviderAlias, } from '../config/agent-config.js';
|
|
@@ -49,6 +49,58 @@ export function catalogOptionId(catalog, model, provider) {
|
|
|
49
49
|
return e.id;
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
|
+
/** TAL-542: the vendor a first-party provider serves, or a model id names by prefix; '' when neither says. */
|
|
53
|
+
const PROVIDER_VENDOR = { openai: 'openai', 'openai-api': 'openai', 'openai-codex': 'openai', anthropic: 'anthropic', gemini: 'google', google: 'google' };
|
|
54
|
+
function modelVendor(model) {
|
|
55
|
+
const m = model.trim().toLowerCase();
|
|
56
|
+
if (m.includes('/'))
|
|
57
|
+
return { openai: 'openai', anthropic: 'anthropic', google: 'google', gemini: 'google' }[m.slice(0, m.indexOf('/'))] ?? '';
|
|
58
|
+
return m.startsWith('gpt') ? 'openai' : m.startsWith('claude') ? 'anthropic' : m.startsWith('gemini') ? 'google' : '';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* TAL-542 (Python `_resolve_compatible_session_model_state` + `_repair_foreign_session_model_provider`): the pair a
|
|
62
|
+
* session's stale model starts on. A model its provider's catalog group does not list moves to the one other provider
|
|
63
|
+
* that lists it; else, when the model names another vendor than a first-party provider, to the profile default. No
|
|
64
|
+
* evidence (no group for the provider, a group built from a failed live lookup in `unlisted`, which never names an owner either) keeps the pair, and so does a stored
|
|
65
|
+
* provider that is the profile's own unless the model names another vendor. `null` keeps the pair.
|
|
66
|
+
*/
|
|
67
|
+
export function repairSessionModel(catalog, model, provider, unlisted = new Set()) {
|
|
68
|
+
const active = canonicaliseProviderId(catalog.active_provider);
|
|
69
|
+
const pid = canonicaliseProviderId(provider) || active;
|
|
70
|
+
if (!model.trim() || !pid || unlisted.has(pid) || !catalog.groups.some((g) => canonicaliseProviderId(g.provider_id) === pid))
|
|
71
|
+
return null;
|
|
72
|
+
const loose = (id) => id.trim().toLowerCase().replaceAll('-', '.');
|
|
73
|
+
const want = loose(model);
|
|
74
|
+
const entries = catalog.groups.flatMap((g) => [...g.models, ...(g.extra_models ?? [])]);
|
|
75
|
+
const tail = (id) => loose(id.slice(id.indexOf('/') + 1));
|
|
76
|
+
const own = entries.filter((e) => canonicaliseProviderId(e.provider_id) === pid).map((e) => str(e.bare_id));
|
|
77
|
+
if (own.some((bare) => loose(bare) === want))
|
|
78
|
+
return null;
|
|
79
|
+
// Equivalence across ids only drops a recognised vendor prefix; another namespace is no evidence. A match starts on the
|
|
80
|
+
// matching entry's own id, so the turn runs on the id that provider advertises.
|
|
81
|
+
const vendorless = (id) => loose(id.replace(/^(openai|anthropic|google|gemini)\//i, ''));
|
|
82
|
+
const same = (bare) => vendorless(bare) === vendorless(model);
|
|
83
|
+
const here = own.find(same);
|
|
84
|
+
if (here)
|
|
85
|
+
return [here, pid];
|
|
86
|
+
// Python `_catalog_model_id_matches`: a model the provider lists under another namespace is kept, never moved.
|
|
87
|
+
if (own.some((bare) => tail(bare) === want))
|
|
88
|
+
return null;
|
|
89
|
+
if (!provider || pid !== active) {
|
|
90
|
+
const owners = new Map(entries.filter((e) => same(str(e.bare_id)) && !unlisted.has(canonicaliseProviderId(e.provider_id))).map((e) => [canonicaliseProviderId(e.provider_id), str(e.bare_id)]));
|
|
91
|
+
const [only] = owners.size === 1 ? [...owners] : [];
|
|
92
|
+
if (only)
|
|
93
|
+
return [only[1], only[0]];
|
|
94
|
+
}
|
|
95
|
+
const vendor = PROVIDER_VENDOR[pid] ?? '';
|
|
96
|
+
const named = modelVendor(model);
|
|
97
|
+
// Python: Codex takes bare ids, so an `openai/` id on it is stale too.
|
|
98
|
+
if (!vendor || !named || (named === vendor && !(pid === 'openai-codex' && model.includes('/'))))
|
|
99
|
+
return null;
|
|
100
|
+
const defaultModel = str(catalog.default_bare_id).trim();
|
|
101
|
+
const defaultProvider = canonicaliseProviderId(catalog.default_provider_id) || active;
|
|
102
|
+
return defaultModel && defaultProvider && (defaultModel !== model || defaultProvider !== pid) ? [defaultModel, defaultProvider] : null;
|
|
103
|
+
}
|
|
52
104
|
/** Python `_split_picker_overflow_models`: past 25 rows the picker shows 15, keeping the selected model visible. */
|
|
53
105
|
export function splitPickerOverflow(models, selected, providerId) {
|
|
54
106
|
if (models.length <= MODEL_PICKER_OVERFLOW_THRESHOLD)
|
|
@@ -98,6 +150,17 @@ export function stampAuxiliarySelections(aux, catalog) {
|
|
|
98
150
|
}
|
|
99
151
|
/** Python `_BESPOKE_CATALOG_PROVIDERS`: cards whose catalog is resolved by their own rule, never the generic live probe. */
|
|
100
152
|
const BESPOKE_CATALOG_PROVIDERS = new Set(['openai-codex', 'nous', 'xai-oauth', 'lmstudio', 'opencode-go']);
|
|
153
|
+
/** A provider's configured endpoint: its `providers.<id>.base_url` (alias-aware), else the active model's `base_url`. */
|
|
154
|
+
const configuredBaseUrl = (pid, config) => {
|
|
155
|
+
const providers = dict(config.providers);
|
|
156
|
+
const cfg = dict(providers[pid] ?? providers[Object.keys(providers).find((k) => providerIdentity(k) === providerIdentity(pid)) ?? '']);
|
|
157
|
+
return str(cfg.base_url).trim() || (activeProviderFromConfig(config) === pid ? str(modelSection(config).base_url).trim() : '');
|
|
158
|
+
};
|
|
159
|
+
/** TAL-603: the credential family a card's badge names; `null` when the card holds no credential. */
|
|
160
|
+
const KEY_SOURCE_KINDS = { env_file: 'env', env_var: 'env', env: 'env', oauth: 'oauth', token: 'oauth', config_yaml: 'config', config: 'config', credential_pool: 'pool', plugin: 'plugin' };
|
|
161
|
+
const keySourceKind = (row) => row.has_key === true && row.key_source !== 'none' ? KEY_SOURCE_KINDS[str(row.key_source)] ?? 'other' : null;
|
|
162
|
+
/** TAL-570: Ollama and LM Studio run keyless, so a configured endpoint makes them ready the way a key does. */
|
|
163
|
+
const selfHostedEndpoint = (pid, config) => SELF_HOSTED_PROVIDER_IDS.has(pid) && Boolean(configuredBaseUrl(pid, config));
|
|
101
164
|
/** Python `_unqualified_model_id`: strip a picker routing hint (`@provider:`). */
|
|
102
165
|
const unqualifiedModelId = (id) => parseProviderQualifiedModel(id)?.[0] ?? id.trim();
|
|
103
166
|
/** Python `_MODEL_PICKER_OVERFLOW_THRESHOLD` / `_MODEL_PICKER_VISIBLE_TARGET`. */
|
|
@@ -106,8 +169,8 @@ const MODEL_PICKER_VISIBLE_TARGET = 15;
|
|
|
106
169
|
const ACCOUNT_USAGE_CACHE_TTL_S = 45;
|
|
107
170
|
const LIVE_TTL_S = 86_400;
|
|
108
171
|
const PROVIDERS_TTL_S = 30;
|
|
109
|
-
|
|
110
|
-
const
|
|
172
|
+
/** The sidecar reads a balance endpoint with a 15 s timeout of its own. */
|
|
173
|
+
const BALANCE_TIMEOUT_MS = 20_000;
|
|
111
174
|
const PLUGIN_LIST_TIMEOUT_MS = 15_000;
|
|
112
175
|
/** TAL-288: the card text for a plugin provider the Agent does not report ready. */
|
|
113
176
|
const PLUGIN_SETUP_ERRORS = {
|
|
@@ -118,6 +181,34 @@ const PLUGIN_SETUP_ERRORS = {
|
|
|
118
181
|
};
|
|
119
182
|
const PLUGIN_NO_MODELS = 'This provider listed no models.';
|
|
120
183
|
const COST_SNAPSHOT_MAX_DAYS = 365;
|
|
184
|
+
/**
|
|
185
|
+
* The last `days` daily snapshots with their spend deltas, bar heights, monthly pace, and budget standing (the legacy
|
|
186
|
+
* Web chart's formulas). A drop in `used` is a credit reset, so that day's delta is its `used`.
|
|
187
|
+
*/
|
|
188
|
+
function costHistoryView(rows, days, budget) {
|
|
189
|
+
const window = rows.slice(-days);
|
|
190
|
+
const deltas = window.map((e, i) => {
|
|
191
|
+
const prev = window[i - 1];
|
|
192
|
+
if (i === 0 || e.used === null || prev?.used === null || prev?.used === undefined)
|
|
193
|
+
return null;
|
|
194
|
+
const delta = e.used - prev.used < 0 ? e.used : e.used - prev.used;
|
|
195
|
+
return Math.abs(delta) < 1e-9 ? 0 : Math.round(delta * 1e6) / 1e6;
|
|
196
|
+
});
|
|
197
|
+
const known = deltas.filter((d) => d !== null);
|
|
198
|
+
const maxDelta = Math.max(...deltas.map((d) => d ?? 0), 1e-9);
|
|
199
|
+
const pace = known.length ? (known.reduce((a, b) => a + b, 0) / known.length) * 30 : null;
|
|
200
|
+
const budgetPercent = pace !== null && pace > 0 && budget !== null && budget > 0 ? Math.round((pace / budget) * 100) : null;
|
|
201
|
+
return {
|
|
202
|
+
snapshots: window.map((e, i) => {
|
|
203
|
+
const delta = deltas[i] ?? null;
|
|
204
|
+
return { date: e.date, used: e.used, delta, bar_percent: delta === null ? 0 : Number(Math.max((delta / maxDelta) * 100, delta > 0 ? 2 : 0).toFixed(1)) };
|
|
205
|
+
}),
|
|
206
|
+
monthly_pace: pace === null ? null : Math.round(pace * 100) / 100,
|
|
207
|
+
has_enough_data: known.length > 0,
|
|
208
|
+
budget_percent: budgetPercent,
|
|
209
|
+
budget_level: budgetPercent === null ? null : budgetPercent >= 100 ? 'over' : budgetPercent >= 80 ? 'warn' : 'ok',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
121
212
|
const FIVE_HOUR_WINDOW_S = 18_000;
|
|
122
213
|
const WEEK_WINDOW_S = 604_800;
|
|
123
214
|
/** Swift `rounded()`: half away from zero. */
|
|
@@ -212,6 +303,46 @@ export function normalizeQuotaWindows(raw, nowSeconds) {
|
|
|
212
303
|
weekly_window_index: index(labelHas('week')),
|
|
213
304
|
};
|
|
214
305
|
}
|
|
306
|
+
/** The burn-rate breakpoints apply once the window is old and used enough and the projection empties it before reset. */
|
|
307
|
+
function projectionEligible(pace, usedPercent, minimumElapsedHours) {
|
|
308
|
+
return pace.elapsed_minutes >= Math.max(0, minimumElapsedHours) * 60 && usedPercent >= 5 && pace.minutes_to_reset > 20
|
|
309
|
+
&& (pace.projected_minutes_to_empty ?? Infinity) < pace.minutes_to_reset;
|
|
310
|
+
}
|
|
311
|
+
/** The App's former `ProviderQuotaUrgencyCalculator.urgency` for one colour basis; a pace basis without a pace falls back to remaining. */
|
|
312
|
+
function quotaLevel(window, status, basis, t) {
|
|
313
|
+
if (status === 'stale')
|
|
314
|
+
return 'stale';
|
|
315
|
+
const remaining = window?.remaining_percent ?? null;
|
|
316
|
+
if (status !== 'available' || remaining === null)
|
|
317
|
+
return status === 'available' ? 'healthy' : 'unavailable';
|
|
318
|
+
const pace = window?.pace ?? null;
|
|
319
|
+
if (basis === 'pace' && pace) {
|
|
320
|
+
const eligible = projectionEligible(pace, 100 - remaining, t.pace_minimum_elapsed_hours);
|
|
321
|
+
if (eligible && pace.burn_rate >= Math.max(0, t.pace_critical_burn_rate_percent) / 100)
|
|
322
|
+
return 'critical';
|
|
323
|
+
if (eligible && pace.burn_rate >= Math.max(0, t.pace_warning_burn_rate_percent) / 100)
|
|
324
|
+
return 'warning';
|
|
325
|
+
return pace.pace_delta_percent <= -Math.max(0, t.pace_tolerance_percent) ? 'warning' : 'healthy';
|
|
326
|
+
}
|
|
327
|
+
if (remaining <= Math.max(0, t.critical_remaining_percent))
|
|
328
|
+
return 'critical';
|
|
329
|
+
if (remaining <= Math.max(0, t.warning_remaining_percent))
|
|
330
|
+
return 'warning';
|
|
331
|
+
return 'healthy';
|
|
332
|
+
}
|
|
333
|
+
export function classifyQuotaSource(source, t) {
|
|
334
|
+
const urgency = (w) => ({ remaining: quotaLevel(w, source.status, 'remaining', t), pace: quotaLevel(w, source.status, 'pace', t) });
|
|
335
|
+
return {
|
|
336
|
+
...source,
|
|
337
|
+
windows: source.windows.map((w) => ({
|
|
338
|
+
...w,
|
|
339
|
+
pace: w.pace && { ...w.pace, status: w.pace.pace_delta_percent <= -Math.max(0, t.pace_tolerance_percent) ? 'over' : w.pace.pace_delta_percent > 1 ? 'under' : 'on' },
|
|
340
|
+
projection_eligible: w.pace !== null && w.remaining_percent !== null && projectionEligible(w.pace, 100 - w.remaining_percent, t.pace_minimum_elapsed_hours),
|
|
341
|
+
urgency: urgency(w),
|
|
342
|
+
})),
|
|
343
|
+
urgency: { remaining: urgency(source.windows[0]).remaining, pace: urgency(source.windows[source.pace_window_index ?? 0]).pace },
|
|
344
|
+
};
|
|
345
|
+
}
|
|
215
346
|
export function displayName(pid) {
|
|
216
347
|
return PROVIDER_DISPLAY[pid] ?? pid.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ');
|
|
217
348
|
}
|
|
@@ -300,6 +431,25 @@ export function deduplicateModelIds(groups) {
|
|
|
300
431
|
m.id = `@${groups[gi].provider_id}:${m.id}`;
|
|
301
432
|
}
|
|
302
433
|
}
|
|
434
|
+
/** OpenCode Go's usage windows: their label and length (Python `_sanitize_opencode_go_account_limits`). */
|
|
435
|
+
const OPENCODE_GO_WINDOWS = { rolling: ['5-hour', FIVE_HOUR_WINDOW_S], weekly: ['Weekly', WEEK_WINDOW_S], monthly: ['Monthly', null] };
|
|
436
|
+
/** One pool account's local state as account limits; `status` is the account's, which its quota source shows. */
|
|
437
|
+
function poolEntryLimits(entry, at) {
|
|
438
|
+
return {
|
|
439
|
+
source: 'local_pool', title: 'Credential pool', status: entry.status, available: entry.status === 'available', plan: null, details: [],
|
|
440
|
+
unavailable_reason: entry.unavailable_reason, retry_after: entry.retry_after, fetched_at: null, ...normalizeQuotaWindows([], at),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/** Python `_local_pool_snapshot`: a provider's pool accounts summarised as account limits. */
|
|
444
|
+
function poolLimits(entries, at) {
|
|
445
|
+
const count = (status) => entries.filter((e) => e.status === status).length;
|
|
446
|
+
const [available, exhausted, dead] = [count('available'), count('exhausted'), count('dead')];
|
|
447
|
+
const details = [`${String(available)}/${String(entries.length)} credentials available`, ...(exhausted ? [`${String(exhausted)} exhausted`] : []), ...(dead ? [`${String(dead)} dead`] : [])];
|
|
448
|
+
return {
|
|
449
|
+
source: 'local_pool', title: 'Credential pool', available: available > 0, plan: null, details, unavailable_reason: available ? null : 'All pool credentials are unavailable.', fetched_at: null,
|
|
450
|
+
...normalizeQuotaWindows([], at),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
303
453
|
/** Quota sources keep the first row per source id, ordered by provider id, account label, then source id (TAL-272). */
|
|
304
454
|
export function uniqueQuotaSources(sources) {
|
|
305
455
|
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
@@ -308,12 +458,17 @@ export function uniqueQuotaSources(sources) {
|
|
|
308
458
|
}
|
|
309
459
|
export class ProviderCatalog {
|
|
310
460
|
deps;
|
|
461
|
+
/** Failed live lookups by home, provider, and the `sourceFingerprint` they ran under (TAL-542). */
|
|
311
462
|
liveFailed = new Set();
|
|
463
|
+
/** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). */
|
|
312
464
|
liveIds = new Map();
|
|
313
465
|
liveInflight = new Map();
|
|
466
|
+
/** `pooled`: the fill's `poolProviders` answer, null when unknown (TAL-638). */
|
|
314
467
|
providersCache = new Map();
|
|
315
468
|
/** TAL-301: the last catalog served per profile home, for the sync session payloads' `model_option_id`. */
|
|
316
469
|
lastModels = new Map();
|
|
470
|
+
/** TAL-542: per `lastModels` catalog, the `sourceFingerprint` it was built from, when, and the providers whose group came from a failed live lookup. */
|
|
471
|
+
lastModelsMeta = new Map();
|
|
317
472
|
constructor(deps) {
|
|
318
473
|
this.deps = deps;
|
|
319
474
|
}
|
|
@@ -336,11 +491,19 @@ export class ProviderCatalog {
|
|
|
336
491
|
this.liveIds.delete(key);
|
|
337
492
|
}
|
|
338
493
|
this.providersCache.clear();
|
|
494
|
+
// Credentials changed: the last pool answers no longer stand in for a failed lookup.
|
|
495
|
+
for (const key of [...this.lastPoolAnswers.keys()])
|
|
496
|
+
if (!profileHome || key.startsWith(`${profileHome}\0`))
|
|
497
|
+
this.lastPoolAnswers.delete(key);
|
|
339
498
|
// The picker catalog changed: option ids rebuild on the next read rather than pairing against the old one.
|
|
340
|
-
if (profileHome)
|
|
499
|
+
if (profileHome) {
|
|
341
500
|
this.lastModels.delete(profileHome);
|
|
342
|
-
|
|
501
|
+
this.lastModelsMeta.delete(profileHome);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
343
504
|
this.lastModels.clear();
|
|
505
|
+
this.lastModelsMeta.clear();
|
|
506
|
+
}
|
|
344
507
|
}
|
|
345
508
|
/** TAL-301: the entry id a stored `(model, provider)` pair selects in the last catalog built for this home; a cold home starts building one. */
|
|
346
509
|
modelOptionFor(profileHome, model, provider) {
|
|
@@ -351,9 +514,58 @@ export class ProviderCatalog {
|
|
|
351
514
|
}
|
|
352
515
|
return catalogOptionId(catalog, model, provider);
|
|
353
516
|
}
|
|
354
|
-
/**
|
|
517
|
+
/**
|
|
518
|
+
* TAL-542: the identity of the files a catalog and its live ids derive from: config.yaml, `.env`, the Agent's sign-in
|
|
519
|
+
* store (`auth.json`, rewritten by an account switch), and the installed plugins directory, so a change outside Web
|
|
520
|
+
* makes both stale.
|
|
521
|
+
*/
|
|
522
|
+
sourceFingerprint(profileHome) {
|
|
523
|
+
const stamp = (name) => {
|
|
524
|
+
try {
|
|
525
|
+
const st = statSync(join(profileHome, name), { bigint: true });
|
|
526
|
+
return `${String(st.mtimeNs)}:${String(st.size)}:${String(st.ino)}`;
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return 'missing';
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
return [this.deps.config.fingerprint(profileHome), ...['.env', 'auth.json', 'plugins'].map(stamp)].join('|');
|
|
533
|
+
}
|
|
534
|
+
/** The last catalog built for this home, younger than `maxAgeS`, while its source files are unchanged since (TAL-542). */
|
|
535
|
+
currentModels(profileHome, maxAgeS = Infinity) {
|
|
536
|
+
const meta = this.lastModelsMeta.get(profileHome);
|
|
537
|
+
return meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* TAL-542: `repairSessionModel` against this home's catalog. Agent-owned sign-in and plugin state has no file to
|
|
541
|
+
* fingerprint, so the repair also needs a catalog no older than the providers listing it read (`PROVIDERS_TTL_S`);
|
|
542
|
+
* a cold or stale one keeps the pair and starts a rebuild.
|
|
543
|
+
*/
|
|
544
|
+
sessionModelRepair(profileHome, model, provider) {
|
|
545
|
+
const catalog = this.currentModels(profileHome, PROVIDERS_TTL_S);
|
|
546
|
+
if (!catalog) {
|
|
547
|
+
void this.warmSessionModelRepair(profileHome);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
return repairSessionModel(catalog, model, provider, this.lastModelsMeta.get(profileHome)?.unconfirmed);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Builds the catalog `sessionModelRepair` reads unless a fresh enough one exists; call it after the request's last
|
|
554
|
+
* await. A source that changed while the build awaited makes that build stale, so it is rebuilt once.
|
|
555
|
+
*/
|
|
556
|
+
async warmSessionModelRepair(profileHome) {
|
|
557
|
+
for (let attempt = 0; attempt < 2 && !this.currentModels(profileHome, PROVIDERS_TTL_S); attempt += 1) {
|
|
558
|
+
try {
|
|
559
|
+
await this.models(profileHome);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return; /* fail closed: the pair is kept */
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** Builds the catalog `modelOptionFor` reads once per home, again after config.yaml or `.env` changes; an unavailable catalog leaves every option id null. */
|
|
355
567
|
async warmModelOptions(profileHome) {
|
|
356
|
-
if (this.
|
|
568
|
+
if (this.currentModels(profileHome))
|
|
357
569
|
return;
|
|
358
570
|
try {
|
|
359
571
|
await this.models(profileHome);
|
|
@@ -364,16 +576,20 @@ export class ProviderCatalog {
|
|
|
364
576
|
* The process-environment value of `name` as a named profile may see it: values `loadStartupEnv` copied from the
|
|
365
577
|
* default profile's `.env` belong to that profile only, so they never count for another profile.
|
|
366
578
|
*/
|
|
367
|
-
|
|
368
|
-
|
|
579
|
+
/** `cleared`: the variable a key delete clears, which also retires its `.env`-owned startup copy (TAL-641). */
|
|
580
|
+
processEnv(name, profileHome, cleared) {
|
|
581
|
+
if ((name === cleared || !this.deps.isRootProfileHome(profileHome)) && homeDotenvKeys(this.deps.env).has(name))
|
|
369
582
|
return undefined;
|
|
370
583
|
return this.deps.env[name];
|
|
371
584
|
}
|
|
372
|
-
/**
|
|
373
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Python `_provider_has_key` minus the credential pool, which `probeKey` reads from the sidecar.
|
|
587
|
+
* `cleared` (TAL-641) answers for after `setProviderKey(…, null)`: that variable's `.env` entry and startup copy are gone.
|
|
588
|
+
*/
|
|
589
|
+
providerHasKey(pid, config, envValues, profileHome, cleared) {
|
|
374
590
|
const envVar = providerEnvVar(pid);
|
|
375
591
|
if (envVar) {
|
|
376
|
-
if (valueCountsAsApiKey(pid, envValues[envVar]) || valueCountsAsApiKey(pid, this.processEnv(envVar, profileHome)))
|
|
592
|
+
if ((envVar !== cleared && valueCountsAsApiKey(pid, envValues[envVar])) || valueCountsAsApiKey(pid, this.processEnv(envVar, profileHome, cleared)))
|
|
377
593
|
return true;
|
|
378
594
|
for (const alias of PROVIDER_ENV_VAR_ALIASES[pid] ?? [])
|
|
379
595
|
if (valueCountsAsApiKey(pid, envValues[alias]) || valueCountsAsApiKey(pid, this.processEnv(alias, profileHome)))
|
|
@@ -407,11 +623,13 @@ export class ProviderCatalog {
|
|
|
407
623
|
return null;
|
|
408
624
|
}
|
|
409
625
|
}
|
|
410
|
-
|
|
626
|
+
/** `pooled`: the providers whose persisted credential pool holds an added account (TAL-638). */
|
|
627
|
+
async probeKey(profileHome, pid, config, envValues, pooled) {
|
|
411
628
|
let hasKey = this.providerHasKey(pid, config, envValues, profileHome);
|
|
412
629
|
let isOauth = OAUTH_PROVIDERS.has(pid);
|
|
413
630
|
let keySource = 'none';
|
|
414
631
|
let authError = null;
|
|
632
|
+
let removable = false;
|
|
415
633
|
if (isOauth) {
|
|
416
634
|
keySource = 'oauth';
|
|
417
635
|
const status = await this.authStatus(profileHome, pid);
|
|
@@ -431,8 +649,10 @@ export class ProviderCatalog {
|
|
|
431
649
|
else if (hasKey) {
|
|
432
650
|
const envVar = providerEnvVar(pid);
|
|
433
651
|
if (envVar) {
|
|
434
|
-
if (valueCountsAsApiKey(pid, envValues[envVar]))
|
|
652
|
+
if (valueCountsAsApiKey(pid, envValues[envVar])) {
|
|
435
653
|
keySource = 'env_file';
|
|
654
|
+
removable = !pooled.has(pid) && !this.providerHasKey(pid, config, envValues, profileHome, envVar);
|
|
655
|
+
}
|
|
436
656
|
else if (valueCountsAsApiKey(pid, this.processEnv(envVar, profileHome)))
|
|
437
657
|
keySource = 'env_var';
|
|
438
658
|
else {
|
|
@@ -452,6 +672,11 @@ export class ProviderCatalog {
|
|
|
452
672
|
else
|
|
453
673
|
keySource = 'config_yaml';
|
|
454
674
|
}
|
|
675
|
+
else if (pooled.has(pid)) {
|
|
676
|
+
// Python `_has_explicit_pool_credentials`: keys added with `hermes auth add` live only in the Agent's pool.
|
|
677
|
+
hasKey = true;
|
|
678
|
+
keySource = 'credential_pool';
|
|
679
|
+
}
|
|
455
680
|
else if (!providerEnvVar(pid) && /^[a-z][a-z0-9_-]{0,63}$/.test(pid)) {
|
|
456
681
|
const status = await this.authStatus(profileHome, pid);
|
|
457
682
|
if (status?.logged_in) {
|
|
@@ -461,25 +686,31 @@ export class ProviderCatalog {
|
|
|
461
686
|
isOauth = true;
|
|
462
687
|
}
|
|
463
688
|
}
|
|
464
|
-
return { hasKey, keySource, authError, isOauth };
|
|
689
|
+
return { hasKey, keySource, authError, isOauth, removable };
|
|
465
690
|
}
|
|
466
691
|
/** Live model ids from the Agent for one provider, cached per profile home. */
|
|
467
692
|
async liveModelIds(profileHome, pid, opts = {}) {
|
|
468
693
|
const key = `${profileHome}\0${pid}`;
|
|
694
|
+
const source = this.sourceFingerprint(profileHome);
|
|
469
695
|
const hit = this.liveIds.get(key);
|
|
470
|
-
if (hit && !opts.force && this.deps.now() - hit.at < LIVE_TTL_S)
|
|
696
|
+
if (hit?.source === source && !opts.force && this.deps.now() - hit.at < LIVE_TTL_S)
|
|
471
697
|
return hit.ids;
|
|
472
|
-
|
|
698
|
+
// A lookup started under another source answers for the old endpoint or credential; this one starts its own.
|
|
699
|
+
const flightKey = `${key}\0${source}`;
|
|
700
|
+
const inflight = this.liveInflight.get(flightKey);
|
|
473
701
|
if (inflight && !opts.force)
|
|
474
702
|
return inflight;
|
|
475
703
|
const sidecar = this.deps.sidecar();
|
|
476
|
-
|
|
704
|
+
// No sidecar is a failed lookup for this source: the ids it keeps showing never confirm a stale-model repair (TAL-542).
|
|
705
|
+
if (!sidecar) {
|
|
706
|
+
this.liveFailed.add(flightKey);
|
|
477
707
|
return hit?.ids ?? [];
|
|
708
|
+
}
|
|
478
709
|
const run = sidecar.call('providers.model_ids', { profile_home: profileHome, provider: pid, ...(opts.force ? { force_refresh: true } : {}) }, { timeoutMs: 30_000 })
|
|
479
|
-
.then((r) => { this.liveIds.set(key, { at: this.deps.now(), ids: r.model_ids }); this.liveFailed.delete(
|
|
480
|
-
.catch((error) => { this.deps.log(`[catalog] live model ids for ${pid} failed: ${str(error.message)}`); this.liveFailed.add(
|
|
481
|
-
.finally(() => { this.liveInflight.delete(
|
|
482
|
-
this.liveInflight.set(
|
|
710
|
+
.then((r) => { this.liveIds.set(key, { at: this.deps.now(), ids: r.model_ids, source }); this.liveFailed.delete(flightKey); return r.model_ids; })
|
|
711
|
+
.catch((error) => { this.deps.log(`[catalog] live model ids for ${pid} failed: ${str(error.message)}`); this.liveFailed.add(flightKey); return hit?.ids ?? []; })
|
|
712
|
+
.finally(() => { this.liveInflight.delete(flightKey); });
|
|
713
|
+
this.liveInflight.set(flightKey, run);
|
|
483
714
|
return run;
|
|
484
715
|
}
|
|
485
716
|
/**
|
|
@@ -501,12 +732,18 @@ export class ProviderCatalog {
|
|
|
501
732
|
}
|
|
502
733
|
/** Python `get_providers`. */
|
|
503
734
|
async providers(profileHome) {
|
|
735
|
+
return structuredClone((await this.providerRows(profileHome)).payload);
|
|
736
|
+
}
|
|
737
|
+
/** The cached `providers` answer, shared with `quotas`; callers never mutate it. */
|
|
738
|
+
async providerRows(profileHome) {
|
|
504
739
|
const config = await this.deps.config.read(profileHome);
|
|
505
740
|
const envValues = loadEnvFile(join(profileHome, '.env'));
|
|
506
741
|
const cacheKey = JSON.stringify([config, envValues]);
|
|
507
742
|
const hit = this.providersCache.get(profileHome);
|
|
508
743
|
if (hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
|
|
509
|
-
return
|
|
744
|
+
return hit;
|
|
745
|
+
const pooledIds = await this.poolProviders(profileHome);
|
|
746
|
+
const pooled = new Set(pooledIds ?? []);
|
|
510
747
|
const active = activeProviderFromConfig(config);
|
|
511
748
|
const known = new Set([...Object.keys(PROVIDER_DISPLAY), ...Object.keys(PROVIDER_MODELS), ...OAUTH_PROVIDERS]);
|
|
512
749
|
// A plugin never stands in for a built-in provider, by id or alias (that would route to the built-in's billing); it
|
|
@@ -522,14 +759,15 @@ export class ProviderCatalog {
|
|
|
522
759
|
}
|
|
523
760
|
const rows = [];
|
|
524
761
|
for (const pid of [...known].sort()) {
|
|
525
|
-
const probe = await this.probeKey(profileHome, pid, config, envValues);
|
|
762
|
+
const probe = await this.probeKey(profileHome, pid, config, envValues, pooled);
|
|
526
763
|
if (pid === 'openai' && !probe.hasKey && looksLikeCodexOauthToken(str(envValues[providerEnvVar('openai') ?? ''] ?? this.deps.env.OPENAI_API_KEY)))
|
|
527
764
|
continue;
|
|
765
|
+
const configured = probe.hasKey || selfHostedEndpoint(pid, config);
|
|
528
766
|
let models = pid === 'openrouter' ? FALLBACK_MODELS.map((m) => ({ id: m.id, label: m.label })) : [...(PROVIDER_MODELS[pid] ?? [])];
|
|
529
767
|
let modelsTotal = models.length;
|
|
530
768
|
// Python: the card prefers the live catalog for every keyed provider (exactly like the picker), keeping the
|
|
531
769
|
// static list as the cold/failed-probe fallback; Nous renders a featured subset with the full count.
|
|
532
|
-
if (
|
|
770
|
+
if (configured && (probe.isOauth || pid === 'lmstudio' || pid === 'nous' || !BESPOKE_CATALOG_PROVIDERS.has(pid))) {
|
|
533
771
|
const live = await this.liveModelIds(profileHome, pid);
|
|
534
772
|
if (live.length) {
|
|
535
773
|
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, []) }));
|
|
@@ -545,17 +783,19 @@ export class ProviderCatalog {
|
|
|
545
783
|
if (pid !== 'nous')
|
|
546
784
|
modelsTotal += added.length;
|
|
547
785
|
}
|
|
548
|
-
const baseUrl =
|
|
786
|
+
const baseUrl = configuredBaseUrl(pid, config) || null;
|
|
549
787
|
rows.push({
|
|
550
788
|
id: pid,
|
|
551
789
|
display_name: displayName(pid),
|
|
552
790
|
has_key: probe.hasKey,
|
|
791
|
+
configured,
|
|
553
792
|
configurable: !probe.isOauth && Boolean(providerEnvVar(pid)),
|
|
554
793
|
is_oauth: probe.isOauth,
|
|
555
794
|
is_plugin_provider: false,
|
|
556
795
|
is_self_hosted: SELF_HOSTED_PROVIDER_IDS.has(pid),
|
|
557
796
|
is_custom: false,
|
|
558
797
|
key_source: probe.keySource,
|
|
798
|
+
removable: probe.removable && !SELF_HOSTED_PROVIDER_IDS.has(pid),
|
|
559
799
|
base_url: baseUrl,
|
|
560
800
|
auth_error: probe.authError,
|
|
561
801
|
env_var: providerEnvVar(pid),
|
|
@@ -568,8 +808,8 @@ export class ProviderCatalog {
|
|
|
568
808
|
const ready = plugin.setup === 'ready';
|
|
569
809
|
const live = ready ? await this.liveModelIds(profileHome, plugin.name) : [];
|
|
570
810
|
rows.push({
|
|
571
|
-
id: plugin.name, display_name: plugin.display_name || plugin.name, has_key: ready,
|
|
572
|
-
configurable: false, is_oauth: false, is_plugin_provider: true, is_self_hosted: false, is_custom: false, key_source: ready ? 'plugin' : 'none',
|
|
811
|
+
id: plugin.name, display_name: plugin.display_name || plugin.name, has_key: ready, configured: ready,
|
|
812
|
+
configurable: false, is_oauth: false, is_plugin_provider: true, is_self_hosted: false, is_custom: false, key_source: ready ? 'plugin' : 'none', removable: false,
|
|
573
813
|
base_url: null, auth_error: plugin.setup === 'ready' ? (live.length ? null : PLUGIN_NO_MODELS) : PLUGIN_SETUP_ERRORS[plugin.setup], env_var: null,
|
|
574
814
|
models: live.map((id) => ({ id, label: labelForModel(id, []) })), models_total: live.length,
|
|
575
815
|
});
|
|
@@ -583,24 +823,34 @@ export class ProviderCatalog {
|
|
|
583
823
|
// Python: an `api_key: ${VAR}` reference counts when the variable resolves.
|
|
584
824
|
const cpKey = str(cp.api_key).trim();
|
|
585
825
|
const envRef = /^\$\{([^}]+)\}$/.exec(cpKey)?.[1] ?? '';
|
|
586
|
-
const
|
|
826
|
+
const yamlKey = !envRef && valueCountsAsApiKey(slug, cp.api_key);
|
|
827
|
+
const hasKey = envRef ? Boolean((this.processEnv(envRef, profileHome) ?? '').trim()) : yamlKey || Boolean(str(cp.key_env).trim() && this.processEnv(str(cp.key_env).trim(), profileHome));
|
|
828
|
+
// TAL-603: a `${VAR}` reference or `key_env` credential comes from the environment.
|
|
829
|
+
const keySource = yamlKey ? 'config_yaml' : hasKey ? 'env_var' : cpKey ? 'config_yaml' : 'none';
|
|
587
830
|
rows.push({
|
|
588
|
-
id: slug, display_name: name, has_key: hasKey,
|
|
589
|
-
configurable: false, is_oauth: false, is_plugin_provider: false, is_self_hosted: false, is_custom: true, key_source:
|
|
831
|
+
id: slug, display_name: name, has_key: hasKey, configured: hasKey,
|
|
832
|
+
configurable: false, is_oauth: false, is_plugin_provider: false, is_self_hosted: false, is_custom: true, key_source: keySource, removable: false,
|
|
590
833
|
base_url: str(cp.base_url).trim() || null, auth_error: null, env_var: null, models: ids.map((id) => ({ id, label: labelForModel(id, []) })), models_total: ids.length,
|
|
591
834
|
});
|
|
592
835
|
}
|
|
593
836
|
// Python `_provider_sort_key`: active first, then `custom:*`, then keyed providers, then the rest (alphabetical within).
|
|
594
837
|
const rank = (p) => (str(p.id) === active ? 0 : str(p.id).startsWith('custom:') ? 1 : p.has_key ? 2 : 3);
|
|
595
838
|
rows.sort((a, b) => rank(a) - rank(b) || (str(a.id) < str(b.id) ? -1 : str(a.id) > str(b.id) ? 1 : 0));
|
|
596
|
-
for (const row of rows)
|
|
839
|
+
for (const row of rows) {
|
|
597
840
|
row.models = stampModelEntries(row.models, str(row.id));
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
841
|
+
// TAL-603: the server decides the Active badge, the key badge and the count; a featured list never outnumbers its total.
|
|
842
|
+
row.is_active = str(row.id) === active;
|
|
843
|
+
row.key_source_kind = keySourceKind(row);
|
|
844
|
+
row.models_total = Math.max(Number(row.models_total), row.models.length);
|
|
845
|
+
}
|
|
846
|
+
const entry = { at: this.deps.now(), key: cacheKey, payload: { providers: rows, active_provider: active }, pooled: pooledIds };
|
|
847
|
+
this.providersCache.set(profileHome, entry);
|
|
848
|
+
return entry;
|
|
601
849
|
}
|
|
602
850
|
/** Python `get_available_models` (static catalog + live ids for keyed providers). */
|
|
603
851
|
async models(profileHome) {
|
|
852
|
+
const source = this.sourceFingerprint(profileHome);
|
|
853
|
+
const unconfirmed = new Set();
|
|
604
854
|
const config = await this.deps.config.read(profileHome);
|
|
605
855
|
const envValues = loadEnvFile(join(profileHome, '.env'));
|
|
606
856
|
const active = activeProviderFromConfig(config);
|
|
@@ -644,11 +894,11 @@ export class ProviderCatalog {
|
|
|
644
894
|
detected.add(canonical);
|
|
645
895
|
}
|
|
646
896
|
// Python: OAuth providers the Agent reports as logged in join the picker with their live catalog (#1567, #2545); so do
|
|
647
|
-
// ready plugin providers (TAL-288), under their own name.
|
|
897
|
+
// ready plugin providers (TAL-288), under their own name, and providers keyed only through the credential pool (TAL-638).
|
|
648
898
|
const signedIn = new Set();
|
|
649
899
|
const pluginNames = new Map();
|
|
650
|
-
for (const row of (await this.
|
|
651
|
-
if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true))
|
|
900
|
+
for (const row of (await this.providerRows(profileHome)).payload.providers) {
|
|
901
|
+
if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true && row.key_source !== 'credential_pool'))
|
|
652
902
|
continue;
|
|
653
903
|
signedIn.add(str(row.id));
|
|
654
904
|
if (row.is_plugin_provider === true)
|
|
@@ -708,12 +958,15 @@ export class ProviderCatalog {
|
|
|
708
958
|
let raw = [];
|
|
709
959
|
if ('models' in providerCfg && providerCfg.models_discovered !== true)
|
|
710
960
|
raw = configuredModelOptions(providerCfg.models);
|
|
711
|
-
if (!raw.length && (this.providerHasKey(pid, config, envValues, profileHome) || signedIn.has(pid))) {
|
|
961
|
+
if (!raw.length && (this.providerHasKey(pid, config, envValues, profileHome) || selfHostedEndpoint(pid, config) || signedIn.has(pid))) {
|
|
712
962
|
const live = await this.liveModelIds(profileHome, pid);
|
|
963
|
+
// TAL-542: a failed lookup leaves this group's list unconfirmed, so the stale-model repair never trusts it.
|
|
964
|
+
if (this.liveFailed.has(`${profileHome}\0${pid}\0${source}`))
|
|
965
|
+
unconfirmed.add(pid);
|
|
713
966
|
if (live.length)
|
|
714
967
|
raw = live.map((id) => ({ id, label: pid === 'nous' ? `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` : labelForModel(id, []) }));
|
|
715
968
|
// Python (#1567): an authenticated Nous account with an empty live catalog shows no group; only a failed lookup falls back to the curated list.
|
|
716
|
-
else if (pid === 'nous' && !this.liveFailed.has(`${profileHome}\0${pid}`))
|
|
969
|
+
else if (pid === 'nous' && !this.liveFailed.has(`${profileHome}\0${pid}\0${source}`))
|
|
717
970
|
continue;
|
|
718
971
|
}
|
|
719
972
|
if (!raw.length)
|
|
@@ -772,15 +1025,16 @@ export class ProviderCatalog {
|
|
|
772
1025
|
const defaults = { active_provider: active, default_model: defaultModel, default_provider_id: defaultProvider, default_bare_id: defaultBare };
|
|
773
1026
|
if (!kept.length && defaultModel) {
|
|
774
1027
|
const providerId = active ?? 'default';
|
|
775
|
-
return this.remember(profileHome, { ...defaults, groups: [{ provider: 'Default', provider_id: providerId, models: stampModelEntries([{ id: defaultModel, label: labelForModel(defaultModel, []) }], providerId) }], aliases: {}, configured_model_badges: {} });
|
|
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: {} });
|
|
776
1029
|
}
|
|
777
1030
|
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) } : {}) }));
|
|
778
|
-
return this.remember(profileHome, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
|
|
1031
|
+
return this.remember(profileHome, { source, unconfirmed }, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
|
|
779
1032
|
}
|
|
780
1033
|
/** Stamps the default's entry id and keeps the catalog for `modelOptionFor`. */
|
|
781
|
-
remember(profileHome, catalog) {
|
|
1034
|
+
remember(profileHome, meta, catalog) {
|
|
782
1035
|
catalog.default_option_id = catalogOptionId(catalog, catalog.default_bare_id ?? null, catalog.default_provider_id ?? null);
|
|
783
1036
|
this.lastModels.set(profileHome, catalog);
|
|
1037
|
+
this.lastModelsMeta.set(profileHome, { ...meta, at: this.deps.now() });
|
|
784
1038
|
return structuredClone(catalog);
|
|
785
1039
|
}
|
|
786
1040
|
/** Python `_configured_model_badges_from_static_catalog`: which picker rows are the main/fallback selections. */
|
|
@@ -905,11 +1159,22 @@ export class ProviderCatalog {
|
|
|
905
1159
|
return v;
|
|
906
1160
|
}
|
|
907
1161
|
}
|
|
908
|
-
|
|
909
|
-
|
|
1162
|
+
// The same config sources `providerHasKey` counts, so a provider shown as keyed resolves its key.
|
|
1163
|
+
const model = modelSection(config);
|
|
1164
|
+
if (canonicaliseProviderId(model.provider) === canonicaliseProviderId(pid) && valueCountsAsApiKey(pid, model.api_key))
|
|
1165
|
+
return str(model.api_key).trim();
|
|
1166
|
+
for (const [key, value] of Object.entries(dict(config.providers))) {
|
|
1167
|
+
if (providerIdentity(key) === providerIdentity(pid) && isDict(value) && valueCountsAsApiKey(pid, value.api_key))
|
|
1168
|
+
return str(value.api_key).trim();
|
|
1169
|
+
}
|
|
1170
|
+
return null;
|
|
910
1171
|
}
|
|
911
|
-
/**
|
|
1172
|
+
/** Account-usage snapshots by profile home, provider, and pool account. */
|
|
912
1173
|
accountUsageCache = new Map();
|
|
1174
|
+
/**
|
|
1175
|
+
* Python `get_provider_quota`. `credential` scopes the read to one credential-pool account (TAL-548): a benched account
|
|
1176
|
+
* answers with its local pool state, and every other read uses that account's key.
|
|
1177
|
+
*/
|
|
913
1178
|
async quota(profileHome, providerRaw, opts = {}) {
|
|
914
1179
|
const at = opts.at ?? this.deps.now();
|
|
915
1180
|
const computed_at = isoAt(at);
|
|
@@ -918,17 +1183,23 @@ export class ProviderCatalog {
|
|
|
918
1183
|
if (!provider)
|
|
919
1184
|
return { computed_at, ok: false, provider: null, display_name: null, supported: false, status: 'unavailable', quota: null, message: 'No active provider is configured.' };
|
|
920
1185
|
const name = displayName(provider);
|
|
1186
|
+
const credential = opts.credential ?? null;
|
|
1187
|
+
const answer = (ok, status, message, extra = {}) => ({ computed_at, ok, provider, display_name: name, supported: true, status, quota: null, ...extra, message });
|
|
1188
|
+
if (credential && credential.status !== 'available') {
|
|
1189
|
+
return answer(false, 'unavailable', `${name} credential is unavailable. ${str(credential.unavailable_reason)}`.trim(), { account_limits: poolEntryLimits(credential, at) });
|
|
1190
|
+
}
|
|
921
1191
|
if (ACCOUNT_USAGE_PROVIDERS.has(provider)) {
|
|
922
1192
|
const sidecar = this.deps.sidecar();
|
|
923
1193
|
let limits = null;
|
|
924
1194
|
// Python `_ACCOUNT_USAGE_CACHE_TTL_SECONDS`: a snapshot answers repeat polls for 45 s unless `refresh` is set.
|
|
925
|
-
const cacheKey = `${profileHome}\0${provider}`;
|
|
1195
|
+
const cacheKey = `${profileHome}\0${provider}\0${credential?.credential_id ?? ''}`;
|
|
926
1196
|
const cached = this.accountUsageCache.get(cacheKey);
|
|
927
1197
|
if (cached && !opts.refresh && this.deps.now() - cached.at <= ACCOUNT_USAGE_CACHE_TTL_S)
|
|
928
1198
|
limits = cached.limits;
|
|
929
1199
|
else if (sidecar) {
|
|
930
1200
|
try {
|
|
931
|
-
const
|
|
1201
|
+
const params = { profile_home: profileHome, provider, ...(opts.refresh ? { refresh: true } : {}), ...(credential ? { credential_id: credential.credential_id } : {}) };
|
|
1202
|
+
const snapshot = (await sidecar.call('usage.account', params, { timeoutMs: 35_000 })).snapshot;
|
|
932
1203
|
if (snapshot)
|
|
933
1204
|
limits = { ...snapshot, title: str(snapshot.title) || 'Account limits', available: snapshot.available && !str(snapshot.unavailable_reason) };
|
|
934
1205
|
}
|
|
@@ -941,40 +1212,98 @@ export class ProviderCatalog {
|
|
|
941
1212
|
if (limits)
|
|
942
1213
|
limits = { ...limits, ...normalizeQuotaWindows(limits.windows, at), fetched_at: isoUtc(limits.fetched_at) };
|
|
943
1214
|
if (limits?.available)
|
|
944
|
-
return
|
|
1215
|
+
return answer(true, limits.stale ? 'stale' : 'available', limits.stale ? `${name} refresh failed; showing last-known account limits.` : `${name} account limits loaded.`, { label: limits.title, account_limits: limits });
|
|
945
1216
|
const reason = str(limits?.unavailable_reason).trim();
|
|
946
|
-
return
|
|
1217
|
+
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
|
+
}
|
|
1219
|
+
if (provider === 'openrouter' || provider === 'deepseek' || provider === 'opencode-go') {
|
|
1220
|
+
const apiKey = this.apiKeyFor(provider, profileHome, config);
|
|
1221
|
+
const key = credential ? { credential_id: credential.credential_id } : apiKey ? { api_key: apiKey } : null;
|
|
1222
|
+
const read = key ? await this.balance(profileHome, provider, key) : null;
|
|
1223
|
+
const status = !key || read?.status === 'no_key' ? 'no_key' : read?.status === 'ok' ? 'ok' : read?.status === 'http_error' ? read.http_status : null;
|
|
1224
|
+
if (provider === 'openrouter') {
|
|
1225
|
+
if (status === 'no_key')
|
|
1226
|
+
return answer(false, 'no_key', 'OpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.');
|
|
1227
|
+
if (status === 'ok' && read?.quota) {
|
|
1228
|
+
// The cost chart follows the configured key, so another pool account's spend never lands in it.
|
|
1229
|
+
if (!credential || credential.matches_api_key)
|
|
1230
|
+
this.recordCostSnapshot(profileHome, read.quota, at);
|
|
1231
|
+
return answer(true, 'available', 'OpenRouter quota status loaded.', { label: 'OpenRouter credits', quota: read.quota });
|
|
1232
|
+
}
|
|
1233
|
+
return status === 401 || status === 403 ? answer(false, 'invalid_key', 'OpenRouter rejected the configured API key.') : answer(false, 'unavailable', 'OpenRouter quota status is temporarily unavailable.');
|
|
1234
|
+
}
|
|
1235
|
+
if (provider === 'deepseek') {
|
|
1236
|
+
if (status === 'no_key')
|
|
1237
|
+
return answer(false, 'no_key', 'DeepSeek balance needs a DEEPSEEK_API_KEY configured on the server.', { balances: [] });
|
|
1238
|
+
if (status === 'ok' && read) {
|
|
1239
|
+
const available = read.is_available === true;
|
|
1240
|
+
return answer(available, available ? 'available' : 'exhausted', available ? 'DeepSeek balance loaded.' : 'DeepSeek balance is exhausted.', { label: 'DeepSeek balance', balances: read.balances });
|
|
1241
|
+
}
|
|
1242
|
+
return status === 401 || status === 403 ? answer(false, 'invalid_key', 'DeepSeek rejected the configured API key.', { balances: [] }) : answer(false, 'unavailable', 'DeepSeek balance is temporarily unavailable.', { balances: [] });
|
|
1243
|
+
}
|
|
1244
|
+
if (status === 'no_key')
|
|
1245
|
+
return answer(false, 'no_key', 'OpenCode Go account limits need an API key configured on the server.', { account_limits: null });
|
|
1246
|
+
if (status === 'ok' && read) {
|
|
1247
|
+
// Python `_sanitize_opencode_go_account_limits`: the rolling, weekly and monthly windows of the Go plan.
|
|
1248
|
+
const windows = read.windows.map((w) => ({ label: OPENCODE_GO_WINDOWS[w.key][0], window_seconds: OPENCODE_GO_WINDOWS[w.key][1], used_percent: w.used_percent, reset_at: w.reset_at, detail: w.rate_limited ? 'Rate limited' : null }));
|
|
1249
|
+
const limits = { provider, source: 'usage_api', title: 'Account limits', plan: 'Go', details: [], available: true, unavailable_reason: null, fetched_at: isoAt(at), ...normalizeQuotaWindows(windows, at) };
|
|
1250
|
+
return answer(true, 'available', 'OpenCode Go account limits loaded.', { label: 'OpenCode Go usage', account_limits: limits });
|
|
1251
|
+
}
|
|
1252
|
+
if (status === 401)
|
|
1253
|
+
return answer(false, 'invalid_key', 'OpenCode Go rejected the configured API key.', { account_limits: null });
|
|
1254
|
+
return answer(false, 'unavailable', status === 403 ? 'OpenCode Go account limits require an active subscription.' : 'OpenCode Go account limits are temporarily unavailable.', { account_limits: null });
|
|
947
1255
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
if (info.kind === 'ok')
|
|
954
|
-
return { computed_at, ok: true, provider, display_name: name, supported: true, status: 'available', label: 'OpenRouter credits', quota: info.quota, message: 'OpenRouter quota status loaded.' };
|
|
955
|
-
const status = info.kind === 'invalid_key' ? 'invalid_key' : 'unavailable';
|
|
956
|
-
return { computed_at, ok: false, provider, display_name: name, supported: true, status, quota: null, message: status === 'invalid_key' ? 'OpenRouter rejected the configured API key.' : 'OpenRouter quota status is temporarily unavailable.' };
|
|
1256
|
+
// 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)) ?? [];
|
|
1258
|
+
if (entries.length) {
|
|
1259
|
+
const limits = credential ? poolEntryLimits(credential, at) : poolLimits(entries, at);
|
|
1260
|
+
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 });
|
|
957
1261
|
}
|
|
958
1262
|
return { computed_at, ok: false, provider, display_name: name, supported: false, status: 'unsupported', quota: null, message: `No verified server-side quota or balance endpoint is available for ${name}.` };
|
|
959
1263
|
}
|
|
960
|
-
|
|
961
|
-
|
|
1264
|
+
/**
|
|
1265
|
+
* TAL-548: the provider's credential-pool accounts as the sidecar reads them locally. Each says whether it holds
|
|
1266
|
+
* `apiKey`, the provider's configured key. Null when the pool is unknown (see `poolLookup`).
|
|
1267
|
+
*/
|
|
1268
|
+
poolEntries(profileHome, pid, apiKey) {
|
|
1269
|
+
return this.poolLookup(profileHome, `pool\0${pid}`, async (sidecar) => (await sidecar.call('usage.pool', { profile_home: profileHome, provider: pid, ...(apiKey ? { api_key: apiKey } : {}) })).entries);
|
|
1270
|
+
}
|
|
1271
|
+
/** The last answer of each credential-pool lookup, by profile home and lookup. */
|
|
1272
|
+
lastPoolAnswers = new Map();
|
|
1273
|
+
/**
|
|
1274
|
+
* TAL-548: one credential-pool lookup through the sidecar. A failed lookup answers with the last answer to the same
|
|
1275
|
+
* lookup, else null: an unknown pool, never an empty one, so no pool account is ever reported as removed.
|
|
1276
|
+
*/
|
|
1277
|
+
async poolLookup(profileHome, lookup, call) {
|
|
1278
|
+
const key = `${profileHome}\0${lookup}`;
|
|
962
1279
|
try {
|
|
963
|
-
const
|
|
964
|
-
if (!
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
let payload = JSON.parse(raw.toString('utf8'));
|
|
970
|
-
if (isDict(payload) && isDict(payload.data))
|
|
971
|
-
payload = payload.data;
|
|
972
|
-
const d = dict(payload);
|
|
973
|
-
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
974
|
-
return { kind: 'ok', quota: { limit_remaining: num(d.limit_remaining), usage: num(d.usage), limit: num(d.limit) }, label: str(d.label).trim() || null };
|
|
1280
|
+
const sidecar = this.deps.sidecar();
|
|
1281
|
+
if (!sidecar)
|
|
1282
|
+
throw new Error('the sidecar is not running');
|
|
1283
|
+
const answer = await call(sidecar);
|
|
1284
|
+
this.lastPoolAnswers.set(key, answer);
|
|
1285
|
+
return answer;
|
|
975
1286
|
}
|
|
976
|
-
catch {
|
|
977
|
-
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
this.deps.log(`[catalog] credential pool lookup ${lookup.replace('\0', ' ')} failed: ${str(error.message)}`);
|
|
1289
|
+
return this.lastPoolAnswers.get(key) ?? null;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/** TAL-548: the providers whose persisted credential pool holds an added account; none when the sidecar cannot say. */
|
|
1293
|
+
poolProviders(profileHome) {
|
|
1294
|
+
return this.poolLookup(profileHome, 'providers', async (sidecar) => (await sidecar.call('usage.pool_providers', { profile_home: profileHome })).providers);
|
|
1295
|
+
}
|
|
1296
|
+
/** TAL-548: one key-based balance read through the sidecar, which resolves a pool account's key itself; null when it fails. */
|
|
1297
|
+
async balance(profileHome, provider, key) {
|
|
1298
|
+
const sidecar = this.deps.sidecar();
|
|
1299
|
+
if (!sidecar)
|
|
1300
|
+
return null;
|
|
1301
|
+
try {
|
|
1302
|
+
return await sidecar.call('usage.balance', { profile_home: profileHome, provider, ...key }, { timeoutMs: BALANCE_TIMEOUT_MS });
|
|
1303
|
+
}
|
|
1304
|
+
catch (error) {
|
|
1305
|
+
this.deps.log(`[catalog] ${provider} balance failed: ${str(error.message)}`);
|
|
1306
|
+
return null;
|
|
978
1307
|
}
|
|
979
1308
|
}
|
|
980
1309
|
quotaScopeCache = null;
|
|
@@ -1010,31 +1339,52 @@ export class ProviderCatalog {
|
|
|
1010
1339
|
return `qscope_${createHash('sha256').update(`${this.quotaServerScopeId()}\0${profile}`).digest('hex').slice(0, 32)}`;
|
|
1011
1340
|
}
|
|
1012
1341
|
/**
|
|
1013
|
-
* Python `get_provider_quotas`:
|
|
1014
|
-
*
|
|
1342
|
+
* Python `get_provider_quotas`: the stable-identity envelope the iOS widget persists. A keyed provider is one source per
|
|
1343
|
+
* credential-pool account, else one for its own key (Python `_quota_source_descriptors`, TAL-548). Each source id appears
|
|
1344
|
+
* once (two custom providers can share a slug), ordered by provider, account label, then source id.
|
|
1015
1345
|
*/
|
|
1016
1346
|
async quotas(profileHome, profile, opts = {}) {
|
|
1017
1347
|
const at = this.deps.now();
|
|
1018
|
-
const status = await this.
|
|
1348
|
+
const { payload: status, pooled: pooledIds } = await this.providerRows(profileHome);
|
|
1019
1349
|
const active = status.active_provider;
|
|
1020
1350
|
const scopeId = this.quotaProfileScopeId(profile);
|
|
1021
|
-
// Python `_quota_source_id(profile, provider, "provider"
|
|
1022
|
-
const sourceId = (pid) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\
|
|
1023
|
-
|
|
1351
|
+
// Python `_quota_source_id(profile, provider, credential_id)`: a pool account's id, or "provider" for the provider's own key.
|
|
1352
|
+
const sourceId = (pid, credential) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\0${credential}`).digest('hex').slice(0, 32)}`;
|
|
1353
|
+
// A provider configured only through `hermes auth add` has no key in .env or config.yaml, yet its pool accounts are
|
|
1354
|
+
// sources, including a signed-out OAuth provider's. An unknown pool (a failed lookup with no earlier answer) never
|
|
1355
|
+
// reports a requested source as missing.
|
|
1356
|
+
let poolUnknown = pooledIds === null;
|
|
1357
|
+
const pooled = new Set(pooledIds ?? []);
|
|
1358
|
+
const config = await this.deps.config.read(profileHome);
|
|
1359
|
+
const perProvider = await Promise.all(status.providers.filter((p) => p.has_key || p.is_custom || pooled.has(str(p.id))).map(async (p) => {
|
|
1360
|
+
const pid = str(p.id);
|
|
1361
|
+
const label = str(p.display_name) || pid;
|
|
1362
|
+
const apiKey = this.apiKeyFor(pid, profileHome, config);
|
|
1363
|
+
const found = await this.poolEntries(profileHome, pid, apiKey);
|
|
1364
|
+
if (!found)
|
|
1365
|
+
poolUnknown = true;
|
|
1366
|
+
const entries = found ?? [];
|
|
1367
|
+
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
|
+
// 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));
|
|
1370
|
+
return ownKey ? [{ source_id: sourceId(pid, 'provider'), provider_id: pid, provider_label: label, account_label: label, credential: null }, ...accounts] : accounts;
|
|
1371
|
+
}));
|
|
1372
|
+
let descriptors = uniqueQuotaSources(perProvider.flat());
|
|
1024
1373
|
const requested = str(opts.sourceId).trim() || null;
|
|
1025
1374
|
if (requested)
|
|
1026
1375
|
descriptors = descriptors.filter((d) => d.source_id === requested);
|
|
1376
|
+
const thresholds = this.deps.quotaThresholds?.(profile) ?? QUOTA_THRESHOLD_DEFAULTS;
|
|
1027
1377
|
const sources = await Promise.all(descriptors.map(async (d) => {
|
|
1028
|
-
const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at });
|
|
1378
|
+
const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at, credential: d.credential });
|
|
1029
1379
|
const limits = dict(q.account_limits);
|
|
1030
|
-
return {
|
|
1380
|
+
return classifyQuotaSource({
|
|
1031
1381
|
source_id: d.source_id, provider_id: d.provider_id, provider_label: d.provider_label, account_label: d.account_label,
|
|
1032
1382
|
is_active_provider: d.provider_id === active, supported: q.supported === true, status: str(limits.status) || str(q.status) || 'unavailable',
|
|
1033
|
-
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 ?? [],
|
|
1383
|
+
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 ?? [],
|
|
1034
1384
|
unavailable_reason: limits.unavailable_reason ?? null, retry_after: limits.retry_after ?? null, fetched_at: limits.fetched_at ?? null, message: q.message ?? null,
|
|
1035
|
-
};
|
|
1385
|
+
}, thresholds);
|
|
1036
1386
|
}));
|
|
1037
|
-
return { version: 1, computed_at: isoAt(at), scope_id: scopeId, profile_id: profile, active_provider: active, requested_source_id: requested, missing_source: Boolean(requested && !descriptors.length), sources };
|
|
1387
|
+
return { version: 1, computed_at: isoAt(at), scope_id: scopeId, profile_id: profile, active_provider: active, requested_source_id: requested, missing_source: Boolean(requested && !descriptors.length && !poolUnknown), sources };
|
|
1038
1388
|
}
|
|
1039
1389
|
/** Python `get_provider_cost_history` (OpenRouter only; daily snapshots under `<home>/cost-snapshots`). */
|
|
1040
1390
|
async costHistory(profileHome, providerRaw, days) {
|
|
@@ -1049,56 +1399,46 @@ export class ProviderCatalog {
|
|
|
1049
1399
|
const apiKey = this.apiKeyFor('openrouter', profileHome, config);
|
|
1050
1400
|
if (!apiKey)
|
|
1051
1401
|
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.' };
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const data = JSON.parse(readFileSync(file, 'utf8'));
|
|
1056
|
-
return (Array.isArray(data.snapshots) ? data.snapshots : []).filter(isDict).map((e) => ({ date: str(e.date).trim(), used: typeof e.used === 'number' ? e.used : null, limit: typeof e.limit === 'number' ? e.limit : null })).filter((e) => e.date).sort((a, b) => (a.date < b.date ? -1 : 1));
|
|
1057
|
-
}
|
|
1058
|
-
catch {
|
|
1059
|
-
return [];
|
|
1060
|
-
}
|
|
1061
|
-
};
|
|
1062
|
-
const deltas = (rows) => {
|
|
1063
|
-
const window = rows.slice(-days);
|
|
1064
|
-
return window.map((e, i) => {
|
|
1065
|
-
let delta = null;
|
|
1066
|
-
const prev = window[i - 1];
|
|
1067
|
-
if (i > 0 && e.used !== null && prev?.used !== null && prev?.used !== undefined) {
|
|
1068
|
-
delta = e.used - prev.used;
|
|
1069
|
-
if (delta < 0)
|
|
1070
|
-
delta = e.used;
|
|
1071
|
-
delta = Math.abs(delta) < 1e-9 ? 0 : Math.round(delta * 1e6) / 1e6;
|
|
1072
|
-
}
|
|
1073
|
-
return { date: e.date, used: e.used, delta };
|
|
1074
|
-
});
|
|
1075
|
-
};
|
|
1076
|
-
const info = await this.fetchOpenRouterKey(apiKey);
|
|
1077
|
-
if (info.kind !== 'ok') {
|
|
1078
|
-
return { ok: false, provider, display_name: name, supported: true, status: 'unavailable', window_days: days, snapshots: deltas(read()), limit: null, label: null, monthly_budget: budget, message: 'OpenRouter cost history is temporarily unavailable. Showing last known data.' };
|
|
1402
|
+
const info = await this.balance(profileHome, 'openrouter', { api_key: apiKey });
|
|
1403
|
+
if (info?.status !== 'ok' || !info.quota) {
|
|
1404
|
+
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.' };
|
|
1079
1405
|
}
|
|
1080
|
-
|
|
1406
|
+
const snapshots = this.recordCostSnapshot(profileHome, info.quota, this.deps.now());
|
|
1407
|
+
return { ok: true, provider, display_name: name, supported: true, status: 'available', window_days: days, ...costHistoryView(snapshots, days, budget), limit: info.quota.limit, label: info.label ?? 'OpenRouter credits', monthly_budget: budget, message: 'OpenRouter cost history loaded.' };
|
|
1408
|
+
}
|
|
1409
|
+
readCostSnapshots(profileHome) {
|
|
1410
|
+
try {
|
|
1411
|
+
const data = JSON.parse(readFileSync(join(profileHome, 'cost-snapshots', 'openrouter.json'), 'utf8'));
|
|
1412
|
+
return (Array.isArray(data.snapshots) ? data.snapshots : []).filter(isDict).map((e) => ({ date: str(e.date).trim(), used: typeof e.used === 'number' ? e.used : null, limit: typeof e.limit === 'number' ? e.limit : null })).filter((e) => e.date).sort((a, b) => (a.date < b.date ? -1 : 1));
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/** Writes or updates the UTC day's OpenRouter usage snapshot; every successful key read (quota or cost history) records one. */
|
|
1419
|
+
recordCostSnapshot(profileHome, quota, at) {
|
|
1420
|
+
let snapshots = this.readCostSnapshots(profileHome);
|
|
1081
1421
|
try {
|
|
1082
|
-
const today =
|
|
1083
|
-
const
|
|
1084
|
-
const limit =
|
|
1422
|
+
const today = isoAt(at).slice(0, 10);
|
|
1423
|
+
const used = finite(quota.usage);
|
|
1424
|
+
const limit = finite(quota.limit);
|
|
1085
1425
|
const existing = snapshots.find((e) => e.date === today);
|
|
1086
1426
|
if (existing) {
|
|
1087
|
-
existing.used =
|
|
1427
|
+
existing.used = used;
|
|
1088
1428
|
existing.limit = limit;
|
|
1089
1429
|
}
|
|
1090
1430
|
else
|
|
1091
|
-
snapshots.push({ date: today, used
|
|
1431
|
+
snapshots.push({ date: today, used, limit });
|
|
1092
1432
|
snapshots.sort((a, b) => (a.date < b.date ? -1 : 1));
|
|
1093
1433
|
if (snapshots.length > COST_SNAPSHOT_MAX_DAYS)
|
|
1094
1434
|
snapshots = snapshots.slice(-COST_SNAPSHOT_MAX_DAYS);
|
|
1095
1435
|
mkdirSync(join(profileHome, 'cost-snapshots'), { recursive: true });
|
|
1096
|
-
atomicWriteText(
|
|
1436
|
+
atomicWriteText(join(profileHome, 'cost-snapshots', 'openrouter.json'), JSON.stringify({ provider: 'openrouter', snapshots }, null, 2));
|
|
1437
|
+
return snapshots;
|
|
1097
1438
|
}
|
|
1098
1439
|
catch {
|
|
1099
|
-
|
|
1440
|
+
return this.readCostSnapshots(profileHome);
|
|
1100
1441
|
}
|
|
1101
|
-
return { ok: true, provider, display_name: name, supported: true, status: 'available', window_days: days, snapshots: deltas(snapshots), limit: info.quota.limit, label: info.label ?? 'OpenRouter credits', monthly_budget: budget, message: 'OpenRouter cost history loaded.' };
|
|
1102
1442
|
}
|
|
1103
1443
|
}
|
|
1104
1444
|
export const envFileExists = (profileHome) => existsSync(join(profileHome, '.env'));
|