@maudecode/talaria-web 1.16.1 → 1.17.2
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 +4 -4
- package/dist/api/automation-raw.js +6 -3
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +137 -9
- package/dist/api/automation-router.js +9 -6
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +51 -0
- package/dist/api/chat-router.js +29 -12
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +12 -1
- package/dist/api/git-router.js +17 -7
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +6 -0
- package/dist/api/raw-routes.js +30 -0
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/sessions-router.d.ts +260 -0
- package/dist/api/sessions-router.js +30 -2
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +101 -2
- package/dist/api/settings-router.js +7 -2
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +11 -3
- package/dist/api/sse-routes.js +144 -64
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.js +6 -2
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +73 -8
- package/dist/api/tools-router.js +9 -7
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +0 -9377
- package/dist/app.js +7 -2
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +3 -0
- package/dist/auth/gate.js.map +1 -1
- package/dist/config/agent-config.d.ts +2 -0
- package/dist/config/agent-config.js +9 -0
- package/dist/config/agent-config.js.map +1 -1
- package/dist/http/context.d.ts +3 -0
- package/dist/http/context.js +10 -4
- package/dist/http/context.js.map +1 -1
- package/dist/onboarding.js +4 -0
- package/dist/onboarding.js.map +1 -1
- package/dist/providers/catalog.d.ts +96 -7
- package/dist/providers/catalog.js +434 -113
- package/dist/providers/catalog.js.map +1 -1
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +27 -4
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +27 -9
- package/dist/runtime.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/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/completions.d.ts +13 -1
- package/dist/sessions/completions.js +63 -11
- 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/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 +6 -2
- package/dist/sessions/list.js +33 -10
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +52 -2
- package/dist/sessions/merge.js +213 -10
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/service.d.ts +44 -2
- package/dist/sessions/service.js +251 -57
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +11 -1
- package/dist/sessions/session.js +23 -2
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/state-db.d.ts +26 -4
- package/dist/sessions/state-db.js +126 -8
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/store.js +5 -0
- 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/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 +76 -1
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn.d.ts +30 -9
- package/dist/sessions/turn.js +247 -78
- package/dist/sessions/turn.js.map +1 -1
- package/dist/settings.d.ts +20 -2
- package/dist/settings.js +40 -2
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/fake.d.ts +2 -2
- 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/test/harness.js +2 -0
- package/dist/test/harness.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 +2 -1
- package/dist/tools/mcp.js +11 -5
- package/dist/tools/mcp.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 +7 -0
- package/dist/tools/updates.js +74 -7
- package/dist/tools/updates.js.map +1 -1
- package/dist/workspace/fs.d.ts +7 -3
- package/dist/workspace/fs.js +5 -1
- package/dist/workspace/fs.js.map +1 -1
- package/dist/workspace/media.js +2 -0
- 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 +5 -4
- package/dist/workspace/rollback.js +12 -9
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/upload.d.ts +2 -0
- package/dist/workspace/upload.js +4 -1
- package/dist/workspace/upload.js.map +1 -1
- package/package.json +2 -2
- package/sidecar/scripts/replay_sidecar.py +1 -1
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/__main__.py +7 -1
- package/sidecar/talaria_sidecar/home.py +39 -0
- package/sidecar/talaria_sidecar/methods/chat.py +422 -38
- package/sidecar/talaria_sidecar/methods/commands.py +15 -2
- package/sidecar/talaria_sidecar/methods/cron.py +10 -6
- package/sidecar/talaria_sidecar/methods/kanban.py +115 -15
- package/sidecar/talaria_sidecar/methods/process.py +60 -1
- package/sidecar/talaria_sidecar/methods/profiles.py +7 -1
- package/sidecar/talaria_sidecar/methods/runtime.py +3 -0
- package/sidecar/talaria_sidecar/methods/text.py +17 -1
- package/sidecar/talaria_sidecar/methods/usage.py +257 -4
- package/static/dist/FILES.txt +126 -125
- package/static/dist/assets/AppShell-fb2td9zi.js +1 -0
- package/static/dist/assets/ChatPage-cj96kiy9.js +25 -0
- package/static/dist/assets/Dialog-kxcg7ard.js +1 -0
- package/static/dist/assets/ExtensionRoute-jf7h0754.js +1 -0
- package/static/dist/assets/HubRoute-gmanpd5y.js +2 -0
- package/static/dist/assets/{Markdown-jqrx5es0.js → Markdown-mdwlvj3w.js} +29 -29
- package/static/dist/assets/RightPanel-ftvrjlcp.js +1 -0
- package/static/dist/assets/Select-bilaypej.js +1 -0
- package/static/dist/assets/SessionListPanel-og8n36qs.js +1 -0
- package/static/dist/assets/SettingsLayout-jonjefmj.js +1 -0
- package/static/dist/assets/States-nmejrwrm.js +1 -0
- package/static/dist/assets/Toaster-nnl8zkt9.js +1 -0
- package/static/dist/assets/_app-gdidzd94.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-opyh9y0v.js +1 -0
- package/static/dist/assets/_app.index-hbarzbjx.js +1 -0
- package/static/dist/assets/_app.insights-luywlarz.js +1 -0
- package/static/dist/assets/_app.kanban-kincnbr8.js +1 -0
- package/static/dist/assets/_app.logs-oi2gr3gs.js +1 -0
- package/static/dist/assets/_app.memory-b6jfivt6.js +1 -0
- package/static/dist/assets/_app.profiles-lpvixmqp.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-hh27wwu2.js → _app.session._sessionId-bfd4h3id.js} +1 -1
- package/static/dist/assets/_app.settings-vowx3iq0.js +1 -0
- package/static/dist/assets/_app.settings._section-cnntg1tu.js +3 -0
- package/static/dist/assets/_app.skills-fb57lpau.js +1 -0
- package/static/dist/assets/_app.tasks-eszpx1q7.js +2 -0
- package/static/dist/assets/_app.todos-nr05ac6w.js +1 -0
- package/static/dist/assets/_app.workspaces-d2luomu6.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-oity9y4o.js → abnfDiagram-VCTEODGH-b2x1lqot.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-ceaepnyr.js → architectureDiagram-5GKGNRK7-h38o5l7b.js} +1 -1
- package/static/dist/assets/{back-eu64w89w.js → back-bemaer09.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-n79st4pj.js → blockDiagram-I7D4REHJ-bqquxv2u.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mi73akzk.js → c4Diagram-7LVT6UL2-f3fb8w6z.js} +1 -1
- package/static/dist/assets/{cancel-mzvkdjg8.js → cancel-drd0g6d9.js} +1 -1
- package/static/dist/assets/channel-cpdgf867.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-laat0g1q.js → chunk-4HAMMTFA-bzfyyzp3.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-0psy1tr2.js → chunk-75Z2AOVW-ly99rfx5.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lw8uox8x.js → chunk-DU6HZSFF-bg6ecfqf.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-erassib3.js → chunk-F27PBJKO-e8k7mmqw.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-fz7rx619.js → chunk-GMAD6QVW-cmalnoaj.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-gdb0epb2.js → chunk-GVQU2GXP-gmif5o01.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-ea8t26qw.js → chunk-IMKFNOWR-f1nwwh6u.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-mb1gu9kz.js → chunk-L3NEJ4N5-oktfh5v7.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-eovmccvy.js → chunk-OSK3NFVY-b1sdx96z.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-bo55zjo5.js → chunk-P2QGCYS3-fhu4nzxt.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-ja7ohe4h.js → chunk-POPQ4Y6H-em31rnef.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-e30g9nus.js → chunk-PWAF6VOD-bxhhyftw.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-gbb798f7.js → chunk-SHT3W25Y-fmrvwcav.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-h2iw8urm.js → chunk-SVP7TREG-lymukl2j.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-fd2cottz.js → chunk-TICWLB2K-lmza6k23.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +1 -0
- package/static/dist/assets/{copied-h65fc3cx.js → copied-ima3pysb.js} +1 -1
- package/static/dist/assets/copy-fw5b1pc6.js +1 -0
- package/static/dist/assets/{cynefin-OW5HDTMX-lbltyl5j.js → cynefin-OW5HDTMX-czlhbyhs.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-lxhsqq4a.js → cynefinDiagram-5FMLGOSQ-gq7mvswb.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-gn4nnaln.js → dagre-GXQ25YYZ-cqdbf7dr.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-pa3ij6q7.js → diagram-S7CK7UJ4-o6arxa58.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-zbipyz57.js → diagram-UQ7AKVKN-nhnd68tz.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-d1kbpzgf.js → diagram-VSXAHHWV-dk2sclzn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-i3ty14xl.js → diagram-VX7I27RA-n3k5nyx3.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-m8xefqa7.js → diagram-Z3DM3KII-m6q9cwdh.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-nz7wj05l.js → ebnfDiagram-PWID7BFC-lsa5641p.js} +1 -1
- package/static/dist/assets/endpoints-d33siokx.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fa41ib83.js → erDiagram-RLTQ6QDP-fo2ckbe6.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-n7zq01qu.js → ganttDiagram-EL5Y4UJY-nzqz6x9g.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-c89emb07.js → gitGraph-4MIJSDKK-gukvcxe6.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-cz7cgr6w.js → gitGraphDiagram-WWUBYQGX-lqhdp8f0.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-d9bapmni.js → highlighted-body-KPVGNVTW-igw6cp39.js} +1 -1
- package/static/dist/assets/index-cxq58xb1.js +11 -0
- package/static/dist/assets/index-hgtx08n5.css +2 -0
- package/static/dist/assets/{info-A6RAGUB7-ndddhhhh.js → info-A6RAGUB7-gvu7a93l.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-ccxigcpf.js → infoDiagram-27XIBGKW-m8t8g6w1.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-le0z9vtc.js → ishikawaDiagram-5VMMS53U-l07vkbgj.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-ivsmb26o.js → journeyDiagram-3NMN7TZE-hybkgj8l.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-h6n2awt0.js → kanban-definition-UXKFOSKX-by13uvtn.js} +1 -1
- package/static/dist/assets/lazyRouteComponent-af0lvgjx.js +1 -0
- package/static/dist/assets/{line-dmzlfe0u.js → line-osxq6fq7.js} +1 -1
- package/static/dist/assets/link-jyxicdhg.js +1 -0
- package/static/dist/assets/{login-kjhasvp8.js → login-ec21t39x.js} +1 -1
- package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-gwyd5bnq.js → mermaid-parser.core-i91oxmou.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-l68gg5h7.js → mindmap-definition-YA3MSWOX-m82mx8ww.js} +1 -1
- package/static/dist/assets/onboarding-btnltvyz.js +1 -0
- package/static/dist/assets/onboarding_model_label-dp7xffoh.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-gpv06065.js → packet-AYTQ26CC-nbtgge2o.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-e52vs23u.js → pegDiagram-XKGWAZYB-f1zxzga1.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-l6m7nz5s.js → pie-WAS4IAKB-of8vuhxs.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-b4is36bq.js → pieDiagram-E7YTZNPT-o7y8ftio.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-m8vt2788.js → quadrantDiagram-AXDQQJYC-iqiwia1t.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-nxbrhiov.js → radar-RG4KPBEZ-delnsy2v.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-222mu9hi.js → railroad-74A4TZTK-nspnyq9u.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-o8lldfzv.js → railroadDiagram-O6MQD6OU-z41tqx5z.js} +1 -1
- package/static/dist/assets/refresh-b4uams1i.js +1 -0
- package/static/dist/assets/refresh-cw-nt1dtsx4.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ovyq38kv.js → requirementDiagram-BXWQKSXE-bdvif194.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-ou7lznor.js → sankeyDiagram-P5KCCOFB-h73e0rol.js} +1 -1
- package/static/dist/assets/{saved-bazmcup9.js → saved-kg52csy7.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-ovckdzf2.js → sequenceDiagram-WJ2MYXX4-iv95mb1z.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-ertwwhlx.js → settings_section_extensions_title-ej0iapxe.js} +1 -1
- package/static/dist/assets/share._token-f97e94r0.js +2 -0
- package/static/dist/assets/sliders-horizontal-fj18it47.js +1 -0
- package/static/dist/assets/sse-fwpqtzbg.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-fcrexkkv.js → stateDiagram-D77RDMKH-k417ixmf.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-f7usu8fw.js → swimlanes-42K2YHIH-esola70w.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-gp1hvz9f.js → timeline-definition-24CTP7MA-getrz2y8.js} +1 -1
- package/static/dist/assets/todoStore-eginf9h4.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-gb9gf11u.js → treeView-Q6P3EWNA-hc7ap1q7.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-nuvfnvvi.js → treemap-WGGIJYW6-b7k1syy5.js} +1 -1
- package/static/dist/assets/tts-lxdxnmjd.js +1 -0
- package/static/dist/assets/{untitled-htelhc68.js → untitled-ovpfthg4.js} +1 -1
- package/static/dist/assets/{useForm-h2t0u14e.js → useForm-bcblv1rx.js} +1 -1
- package/static/dist/assets/useLocale-fi5jb8ma.js +1 -0
- package/static/dist/assets/{useParams-dqd64jv7.js → useParams-lhg566ew.js} +1 -1
- package/static/dist/assets/{useSearch-j30pvsy7.js → useSearch-lbrh6ejn.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-fzchbzaj.js → vennDiagram-4TSXK5OY-n6pcy5qb.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-m47mhfbh.js → wardley-WFR3VGLG-lpa18vz1.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-odg6ct1z.js → wardleyDiagram-VM6X3IG4-kpclwwg7.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mrhms5um.js → xychartDiagram-S5SC5T6Z-paa2n8y0.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/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,14 @@ 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-570: Ollama and LM Studio run keyless, so a configured endpoint makes them ready the way a key does. */
|
|
160
|
+
const selfHostedEndpoint = (pid, config) => SELF_HOSTED_PROVIDER_IDS.has(pid) && Boolean(configuredBaseUrl(pid, config));
|
|
101
161
|
/** Python `_unqualified_model_id`: strip a picker routing hint (`@provider:`). */
|
|
102
162
|
const unqualifiedModelId = (id) => parseProviderQualifiedModel(id)?.[0] ?? id.trim();
|
|
103
163
|
/** Python `_MODEL_PICKER_OVERFLOW_THRESHOLD` / `_MODEL_PICKER_VISIBLE_TARGET`. */
|
|
@@ -106,8 +166,8 @@ const MODEL_PICKER_VISIBLE_TARGET = 15;
|
|
|
106
166
|
const ACCOUNT_USAGE_CACHE_TTL_S = 45;
|
|
107
167
|
const LIVE_TTL_S = 86_400;
|
|
108
168
|
const PROVIDERS_TTL_S = 30;
|
|
109
|
-
|
|
110
|
-
const
|
|
169
|
+
/** The sidecar reads a balance endpoint with a 15 s timeout of its own. */
|
|
170
|
+
const BALANCE_TIMEOUT_MS = 20_000;
|
|
111
171
|
const PLUGIN_LIST_TIMEOUT_MS = 15_000;
|
|
112
172
|
/** TAL-288: the card text for a plugin provider the Agent does not report ready. */
|
|
113
173
|
const PLUGIN_SETUP_ERRORS = {
|
|
@@ -118,6 +178,34 @@ const PLUGIN_SETUP_ERRORS = {
|
|
|
118
178
|
};
|
|
119
179
|
const PLUGIN_NO_MODELS = 'This provider listed no models.';
|
|
120
180
|
const COST_SNAPSHOT_MAX_DAYS = 365;
|
|
181
|
+
/**
|
|
182
|
+
* The last `days` daily snapshots with their spend deltas, bar heights, monthly pace, and budget standing (the legacy
|
|
183
|
+
* Web chart's formulas). A drop in `used` is a credit reset, so that day's delta is its `used`.
|
|
184
|
+
*/
|
|
185
|
+
function costHistoryView(rows, days, budget) {
|
|
186
|
+
const window = rows.slice(-days);
|
|
187
|
+
const deltas = window.map((e, i) => {
|
|
188
|
+
const prev = window[i - 1];
|
|
189
|
+
if (i === 0 || e.used === null || prev?.used === null || prev?.used === undefined)
|
|
190
|
+
return null;
|
|
191
|
+
const delta = e.used - prev.used < 0 ? e.used : e.used - prev.used;
|
|
192
|
+
return Math.abs(delta) < 1e-9 ? 0 : Math.round(delta * 1e6) / 1e6;
|
|
193
|
+
});
|
|
194
|
+
const known = deltas.filter((d) => d !== null);
|
|
195
|
+
const maxDelta = Math.max(...deltas.map((d) => d ?? 0), 1e-9);
|
|
196
|
+
const pace = known.length ? (known.reduce((a, b) => a + b, 0) / known.length) * 30 : null;
|
|
197
|
+
const budgetPercent = pace !== null && pace > 0 && budget !== null && budget > 0 ? Math.round((pace / budget) * 100) : null;
|
|
198
|
+
return {
|
|
199
|
+
snapshots: window.map((e, i) => {
|
|
200
|
+
const delta = deltas[i] ?? null;
|
|
201
|
+
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)) };
|
|
202
|
+
}),
|
|
203
|
+
monthly_pace: pace === null ? null : Math.round(pace * 100) / 100,
|
|
204
|
+
has_enough_data: known.length > 0,
|
|
205
|
+
budget_percent: budgetPercent,
|
|
206
|
+
budget_level: budgetPercent === null ? null : budgetPercent >= 100 ? 'over' : budgetPercent >= 80 ? 'warn' : 'ok',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
121
209
|
const FIVE_HOUR_WINDOW_S = 18_000;
|
|
122
210
|
const WEEK_WINDOW_S = 604_800;
|
|
123
211
|
/** Swift `rounded()`: half away from zero. */
|
|
@@ -212,6 +300,46 @@ export function normalizeQuotaWindows(raw, nowSeconds) {
|
|
|
212
300
|
weekly_window_index: index(labelHas('week')),
|
|
213
301
|
};
|
|
214
302
|
}
|
|
303
|
+
/** The burn-rate breakpoints apply once the window is old and used enough and the projection empties it before reset. */
|
|
304
|
+
function projectionEligible(pace, usedPercent, minimumElapsedHours) {
|
|
305
|
+
return pace.elapsed_minutes >= Math.max(0, minimumElapsedHours) * 60 && usedPercent >= 5 && pace.minutes_to_reset > 20
|
|
306
|
+
&& (pace.projected_minutes_to_empty ?? Infinity) < pace.minutes_to_reset;
|
|
307
|
+
}
|
|
308
|
+
/** The App's former `ProviderQuotaUrgencyCalculator.urgency` for one colour basis; a pace basis without a pace falls back to remaining. */
|
|
309
|
+
function quotaLevel(window, status, basis, t) {
|
|
310
|
+
if (status === 'stale')
|
|
311
|
+
return 'stale';
|
|
312
|
+
const remaining = window?.remaining_percent ?? null;
|
|
313
|
+
if (status !== 'available' || remaining === null)
|
|
314
|
+
return status === 'available' ? 'healthy' : 'unavailable';
|
|
315
|
+
const pace = window?.pace ?? null;
|
|
316
|
+
if (basis === 'pace' && pace) {
|
|
317
|
+
const eligible = projectionEligible(pace, 100 - remaining, t.pace_minimum_elapsed_hours);
|
|
318
|
+
if (eligible && pace.burn_rate >= Math.max(0, t.pace_critical_burn_rate_percent) / 100)
|
|
319
|
+
return 'critical';
|
|
320
|
+
if (eligible && pace.burn_rate >= Math.max(0, t.pace_warning_burn_rate_percent) / 100)
|
|
321
|
+
return 'warning';
|
|
322
|
+
return pace.pace_delta_percent <= -Math.max(0, t.pace_tolerance_percent) ? 'warning' : 'healthy';
|
|
323
|
+
}
|
|
324
|
+
if (remaining <= Math.max(0, t.critical_remaining_percent))
|
|
325
|
+
return 'critical';
|
|
326
|
+
if (remaining <= Math.max(0, t.warning_remaining_percent))
|
|
327
|
+
return 'warning';
|
|
328
|
+
return 'healthy';
|
|
329
|
+
}
|
|
330
|
+
export function classifyQuotaSource(source, t) {
|
|
331
|
+
const urgency = (w) => ({ remaining: quotaLevel(w, source.status, 'remaining', t), pace: quotaLevel(w, source.status, 'pace', t) });
|
|
332
|
+
return {
|
|
333
|
+
...source,
|
|
334
|
+
windows: source.windows.map((w) => ({
|
|
335
|
+
...w,
|
|
336
|
+
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' },
|
|
337
|
+
projection_eligible: w.pace !== null && w.remaining_percent !== null && projectionEligible(w.pace, 100 - w.remaining_percent, t.pace_minimum_elapsed_hours),
|
|
338
|
+
urgency: urgency(w),
|
|
339
|
+
})),
|
|
340
|
+
urgency: { remaining: urgency(source.windows[0]).remaining, pace: urgency(source.windows[source.pace_window_index ?? 0]).pace },
|
|
341
|
+
};
|
|
342
|
+
}
|
|
215
343
|
export function displayName(pid) {
|
|
216
344
|
return PROVIDER_DISPLAY[pid] ?? pid.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)).join(' ');
|
|
217
345
|
}
|
|
@@ -300,6 +428,25 @@ export function deduplicateModelIds(groups) {
|
|
|
300
428
|
m.id = `@${groups[gi].provider_id}:${m.id}`;
|
|
301
429
|
}
|
|
302
430
|
}
|
|
431
|
+
/** OpenCode Go's usage windows: their label and length (Python `_sanitize_opencode_go_account_limits`). */
|
|
432
|
+
const OPENCODE_GO_WINDOWS = { rolling: ['5-hour', FIVE_HOUR_WINDOW_S], weekly: ['Weekly', WEEK_WINDOW_S], monthly: ['Monthly', null] };
|
|
433
|
+
/** One pool account's local state as account limits; `status` is the account's, which its quota source shows. */
|
|
434
|
+
function poolEntryLimits(entry, at) {
|
|
435
|
+
return {
|
|
436
|
+
source: 'local_pool', title: 'Credential pool', status: entry.status, available: entry.status === 'available', plan: null, details: [],
|
|
437
|
+
unavailable_reason: entry.unavailable_reason, retry_after: entry.retry_after, fetched_at: null, ...normalizeQuotaWindows([], at),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/** Python `_local_pool_snapshot`: a provider's pool accounts summarised as account limits. */
|
|
441
|
+
function poolLimits(entries, at) {
|
|
442
|
+
const count = (status) => entries.filter((e) => e.status === status).length;
|
|
443
|
+
const [available, exhausted, dead] = [count('available'), count('exhausted'), count('dead')];
|
|
444
|
+
const details = [`${String(available)}/${String(entries.length)} credentials available`, ...(exhausted ? [`${String(exhausted)} exhausted`] : []), ...(dead ? [`${String(dead)} dead`] : [])];
|
|
445
|
+
return {
|
|
446
|
+
source: 'local_pool', title: 'Credential pool', available: available > 0, plan: null, details, unavailable_reason: available ? null : 'All pool credentials are unavailable.', fetched_at: null,
|
|
447
|
+
...normalizeQuotaWindows([], at),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
303
450
|
/** Quota sources keep the first row per source id, ordered by provider id, account label, then source id (TAL-272). */
|
|
304
451
|
export function uniqueQuotaSources(sources) {
|
|
305
452
|
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
@@ -308,12 +455,17 @@ export function uniqueQuotaSources(sources) {
|
|
|
308
455
|
}
|
|
309
456
|
export class ProviderCatalog {
|
|
310
457
|
deps;
|
|
458
|
+
/** Failed live lookups by home, provider, and the `sourceFingerprint` they ran under (TAL-542). */
|
|
311
459
|
liveFailed = new Set();
|
|
460
|
+
/** `source`: the `sourceFingerprint` the ids were fetched under; another one makes them stale (TAL-542). */
|
|
312
461
|
liveIds = new Map();
|
|
313
462
|
liveInflight = new Map();
|
|
463
|
+
/** `pooled`: the fill's `poolProviders` answer, null when unknown (TAL-638). */
|
|
314
464
|
providersCache = new Map();
|
|
315
465
|
/** TAL-301: the last catalog served per profile home, for the sync session payloads' `model_option_id`. */
|
|
316
466
|
lastModels = new Map();
|
|
467
|
+
/** TAL-542: per `lastModels` catalog, the `sourceFingerprint` it was built from, when, and the providers whose group came from a failed live lookup. */
|
|
468
|
+
lastModelsMeta = new Map();
|
|
317
469
|
constructor(deps) {
|
|
318
470
|
this.deps = deps;
|
|
319
471
|
}
|
|
@@ -336,11 +488,19 @@ export class ProviderCatalog {
|
|
|
336
488
|
this.liveIds.delete(key);
|
|
337
489
|
}
|
|
338
490
|
this.providersCache.clear();
|
|
491
|
+
// Credentials changed: the last pool answers no longer stand in for a failed lookup.
|
|
492
|
+
for (const key of [...this.lastPoolAnswers.keys()])
|
|
493
|
+
if (!profileHome || key.startsWith(`${profileHome}\0`))
|
|
494
|
+
this.lastPoolAnswers.delete(key);
|
|
339
495
|
// The picker catalog changed: option ids rebuild on the next read rather than pairing against the old one.
|
|
340
|
-
if (profileHome)
|
|
496
|
+
if (profileHome) {
|
|
341
497
|
this.lastModels.delete(profileHome);
|
|
342
|
-
|
|
498
|
+
this.lastModelsMeta.delete(profileHome);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
343
501
|
this.lastModels.clear();
|
|
502
|
+
this.lastModelsMeta.clear();
|
|
503
|
+
}
|
|
344
504
|
}
|
|
345
505
|
/** 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
506
|
modelOptionFor(profileHome, model, provider) {
|
|
@@ -351,9 +511,58 @@ export class ProviderCatalog {
|
|
|
351
511
|
}
|
|
352
512
|
return catalogOptionId(catalog, model, provider);
|
|
353
513
|
}
|
|
354
|
-
/**
|
|
514
|
+
/**
|
|
515
|
+
* TAL-542: the identity of the files a catalog and its live ids derive from: config.yaml, `.env`, the Agent's sign-in
|
|
516
|
+
* store (`auth.json`, rewritten by an account switch), and the installed plugins directory, so a change outside Web
|
|
517
|
+
* makes both stale.
|
|
518
|
+
*/
|
|
519
|
+
sourceFingerprint(profileHome) {
|
|
520
|
+
const stamp = (name) => {
|
|
521
|
+
try {
|
|
522
|
+
const st = statSync(join(profileHome, name), { bigint: true });
|
|
523
|
+
return `${String(st.mtimeNs)}:${String(st.size)}:${String(st.ino)}`;
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return 'missing';
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
return [this.deps.config.fingerprint(profileHome), ...['.env', 'auth.json', 'plugins'].map(stamp)].join('|');
|
|
530
|
+
}
|
|
531
|
+
/** The last catalog built for this home, younger than `maxAgeS`, while its source files are unchanged since (TAL-542). */
|
|
532
|
+
currentModels(profileHome, maxAgeS = Infinity) {
|
|
533
|
+
const meta = this.lastModelsMeta.get(profileHome);
|
|
534
|
+
return meta?.source === this.sourceFingerprint(profileHome) && this.deps.now() - meta.at < maxAgeS ? this.lastModels.get(profileHome) : undefined;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* TAL-542: `repairSessionModel` against this home's catalog. Agent-owned sign-in and plugin state has no file to
|
|
538
|
+
* fingerprint, so the repair also needs a catalog no older than the providers listing it read (`PROVIDERS_TTL_S`);
|
|
539
|
+
* a cold or stale one keeps the pair and starts a rebuild.
|
|
540
|
+
*/
|
|
541
|
+
sessionModelRepair(profileHome, model, provider) {
|
|
542
|
+
const catalog = this.currentModels(profileHome, PROVIDERS_TTL_S);
|
|
543
|
+
if (!catalog) {
|
|
544
|
+
void this.warmSessionModelRepair(profileHome);
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
return repairSessionModel(catalog, model, provider, this.lastModelsMeta.get(profileHome)?.unconfirmed);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Builds the catalog `sessionModelRepair` reads unless a fresh enough one exists; call it after the request's last
|
|
551
|
+
* await. A source that changed while the build awaited makes that build stale, so it is rebuilt once.
|
|
552
|
+
*/
|
|
553
|
+
async warmSessionModelRepair(profileHome) {
|
|
554
|
+
for (let attempt = 0; attempt < 2 && !this.currentModels(profileHome, PROVIDERS_TTL_S); attempt += 1) {
|
|
555
|
+
try {
|
|
556
|
+
await this.models(profileHome);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return; /* fail closed: the pair is kept */
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/** Builds the catalog `modelOptionFor` reads once per home, again after config.yaml or `.env` changes; an unavailable catalog leaves every option id null. */
|
|
355
564
|
async warmModelOptions(profileHome) {
|
|
356
|
-
if (this.
|
|
565
|
+
if (this.currentModels(profileHome))
|
|
357
566
|
return;
|
|
358
567
|
try {
|
|
359
568
|
await this.models(profileHome);
|
|
@@ -369,7 +578,7 @@ export class ProviderCatalog {
|
|
|
369
578
|
return undefined;
|
|
370
579
|
return this.deps.env[name];
|
|
371
580
|
}
|
|
372
|
-
/** Python `_provider_has_key` minus the credential pool
|
|
581
|
+
/** Python `_provider_has_key` minus the credential pool, which `probeKey` reads from the sidecar. */
|
|
373
582
|
providerHasKey(pid, config, envValues, profileHome) {
|
|
374
583
|
const envVar = providerEnvVar(pid);
|
|
375
584
|
if (envVar) {
|
|
@@ -407,7 +616,8 @@ export class ProviderCatalog {
|
|
|
407
616
|
return null;
|
|
408
617
|
}
|
|
409
618
|
}
|
|
410
|
-
|
|
619
|
+
/** `pooled`: the providers whose persisted credential pool holds an added account (TAL-638). */
|
|
620
|
+
async probeKey(profileHome, pid, config, envValues, pooled) {
|
|
411
621
|
let hasKey = this.providerHasKey(pid, config, envValues, profileHome);
|
|
412
622
|
let isOauth = OAUTH_PROVIDERS.has(pid);
|
|
413
623
|
let keySource = 'none';
|
|
@@ -452,6 +662,11 @@ export class ProviderCatalog {
|
|
|
452
662
|
else
|
|
453
663
|
keySource = 'config_yaml';
|
|
454
664
|
}
|
|
665
|
+
else if (pooled.has(pid)) {
|
|
666
|
+
// Python `_has_explicit_pool_credentials`: keys added with `hermes auth add` live only in the Agent's pool.
|
|
667
|
+
hasKey = true;
|
|
668
|
+
keySource = 'credential_pool';
|
|
669
|
+
}
|
|
455
670
|
else if (!providerEnvVar(pid) && /^[a-z][a-z0-9_-]{0,63}$/.test(pid)) {
|
|
456
671
|
const status = await this.authStatus(profileHome, pid);
|
|
457
672
|
if (status?.logged_in) {
|
|
@@ -466,20 +681,26 @@ export class ProviderCatalog {
|
|
|
466
681
|
/** Live model ids from the Agent for one provider, cached per profile home. */
|
|
467
682
|
async liveModelIds(profileHome, pid, opts = {}) {
|
|
468
683
|
const key = `${profileHome}\0${pid}`;
|
|
684
|
+
const source = this.sourceFingerprint(profileHome);
|
|
469
685
|
const hit = this.liveIds.get(key);
|
|
470
|
-
if (hit && !opts.force && this.deps.now() - hit.at < LIVE_TTL_S)
|
|
686
|
+
if (hit?.source === source && !opts.force && this.deps.now() - hit.at < LIVE_TTL_S)
|
|
471
687
|
return hit.ids;
|
|
472
|
-
|
|
688
|
+
// A lookup started under another source answers for the old endpoint or credential; this one starts its own.
|
|
689
|
+
const flightKey = `${key}\0${source}`;
|
|
690
|
+
const inflight = this.liveInflight.get(flightKey);
|
|
473
691
|
if (inflight && !opts.force)
|
|
474
692
|
return inflight;
|
|
475
693
|
const sidecar = this.deps.sidecar();
|
|
476
|
-
|
|
694
|
+
// No sidecar is a failed lookup for this source: the ids it keeps showing never confirm a stale-model repair (TAL-542).
|
|
695
|
+
if (!sidecar) {
|
|
696
|
+
this.liveFailed.add(flightKey);
|
|
477
697
|
return hit?.ids ?? [];
|
|
698
|
+
}
|
|
478
699
|
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(
|
|
700
|
+
.then((r) => { this.liveIds.set(key, { at: this.deps.now(), ids: r.model_ids, source }); this.liveFailed.delete(flightKey); return r.model_ids; })
|
|
701
|
+
.catch((error) => { this.deps.log(`[catalog] live model ids for ${pid} failed: ${str(error.message)}`); this.liveFailed.add(flightKey); return hit?.ids ?? []; })
|
|
702
|
+
.finally(() => { this.liveInflight.delete(flightKey); });
|
|
703
|
+
this.liveInflight.set(flightKey, run);
|
|
483
704
|
return run;
|
|
484
705
|
}
|
|
485
706
|
/**
|
|
@@ -501,12 +722,18 @@ export class ProviderCatalog {
|
|
|
501
722
|
}
|
|
502
723
|
/** Python `get_providers`. */
|
|
503
724
|
async providers(profileHome) {
|
|
725
|
+
return structuredClone((await this.providerRows(profileHome)).payload);
|
|
726
|
+
}
|
|
727
|
+
/** The cached `providers` answer, shared with `quotas`; callers never mutate it. */
|
|
728
|
+
async providerRows(profileHome) {
|
|
504
729
|
const config = await this.deps.config.read(profileHome);
|
|
505
730
|
const envValues = loadEnvFile(join(profileHome, '.env'));
|
|
506
731
|
const cacheKey = JSON.stringify([config, envValues]);
|
|
507
732
|
const hit = this.providersCache.get(profileHome);
|
|
508
733
|
if (hit?.key === cacheKey && this.deps.now() - hit.at < PROVIDERS_TTL_S)
|
|
509
|
-
return
|
|
734
|
+
return hit;
|
|
735
|
+
const pooledIds = await this.poolProviders(profileHome);
|
|
736
|
+
const pooled = new Set(pooledIds ?? []);
|
|
510
737
|
const active = activeProviderFromConfig(config);
|
|
511
738
|
const known = new Set([...Object.keys(PROVIDER_DISPLAY), ...Object.keys(PROVIDER_MODELS), ...OAUTH_PROVIDERS]);
|
|
512
739
|
// 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 +749,15 @@ export class ProviderCatalog {
|
|
|
522
749
|
}
|
|
523
750
|
const rows = [];
|
|
524
751
|
for (const pid of [...known].sort()) {
|
|
525
|
-
const probe = await this.probeKey(profileHome, pid, config, envValues);
|
|
752
|
+
const probe = await this.probeKey(profileHome, pid, config, envValues, pooled);
|
|
526
753
|
if (pid === 'openai' && !probe.hasKey && looksLikeCodexOauthToken(str(envValues[providerEnvVar('openai') ?? ''] ?? this.deps.env.OPENAI_API_KEY)))
|
|
527
754
|
continue;
|
|
755
|
+
const configured = probe.hasKey || selfHostedEndpoint(pid, config);
|
|
528
756
|
let models = pid === 'openrouter' ? FALLBACK_MODELS.map((m) => ({ id: m.id, label: m.label })) : [...(PROVIDER_MODELS[pid] ?? [])];
|
|
529
757
|
let modelsTotal = models.length;
|
|
530
758
|
// Python: the card prefers the live catalog for every keyed provider (exactly like the picker), keeping the
|
|
531
759
|
// static list as the cold/failed-probe fallback; Nous renders a featured subset with the full count.
|
|
532
|
-
if (
|
|
760
|
+
if (configured && (probe.isOauth || pid === 'lmstudio' || pid === 'nous' || !BESPOKE_CATALOG_PROVIDERS.has(pid))) {
|
|
533
761
|
const live = await this.liveModelIds(profileHome, pid);
|
|
534
762
|
if (live.length) {
|
|
535
763
|
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,11 +773,12 @@ export class ProviderCatalog {
|
|
|
545
773
|
if (pid !== 'nous')
|
|
546
774
|
modelsTotal += added.length;
|
|
547
775
|
}
|
|
548
|
-
const baseUrl =
|
|
776
|
+
const baseUrl = configuredBaseUrl(pid, config) || null;
|
|
549
777
|
rows.push({
|
|
550
778
|
id: pid,
|
|
551
779
|
display_name: displayName(pid),
|
|
552
780
|
has_key: probe.hasKey,
|
|
781
|
+
configured,
|
|
553
782
|
configurable: !probe.isOauth && Boolean(providerEnvVar(pid)),
|
|
554
783
|
is_oauth: probe.isOauth,
|
|
555
784
|
is_plugin_provider: false,
|
|
@@ -568,7 +797,7 @@ export class ProviderCatalog {
|
|
|
568
797
|
const ready = plugin.setup === 'ready';
|
|
569
798
|
const live = ready ? await this.liveModelIds(profileHome, plugin.name) : [];
|
|
570
799
|
rows.push({
|
|
571
|
-
id: plugin.name, display_name: plugin.display_name || plugin.name, has_key: ready,
|
|
800
|
+
id: plugin.name, display_name: plugin.display_name || plugin.name, has_key: ready, configured: ready,
|
|
572
801
|
configurable: false, is_oauth: false, is_plugin_provider: true, is_self_hosted: false, is_custom: false, key_source: ready ? 'plugin' : 'none',
|
|
573
802
|
base_url: null, auth_error: plugin.setup === 'ready' ? (live.length ? null : PLUGIN_NO_MODELS) : PLUGIN_SETUP_ERRORS[plugin.setup], env_var: null,
|
|
574
803
|
models: live.map((id) => ({ id, label: labelForModel(id, []) })), models_total: live.length,
|
|
@@ -585,7 +814,7 @@ export class ProviderCatalog {
|
|
|
585
814
|
const envRef = /^\$\{([^}]+)\}$/.exec(cpKey)?.[1] ?? '';
|
|
586
815
|
const hasKey = envRef ? Boolean((this.processEnv(envRef, profileHome) ?? '').trim()) : valueCountsAsApiKey(slug, cp.api_key) || Boolean(str(cp.key_env).trim() && this.processEnv(str(cp.key_env).trim(), profileHome));
|
|
587
816
|
rows.push({
|
|
588
|
-
id: slug, display_name: name, has_key: hasKey,
|
|
817
|
+
id: slug, display_name: name, has_key: hasKey, configured: hasKey,
|
|
589
818
|
configurable: false, is_oauth: false, is_plugin_provider: false, is_self_hosted: false, is_custom: true, key_source: str(cp.api_key).trim() ? 'config_yaml' : 'none',
|
|
590
819
|
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
820
|
});
|
|
@@ -595,12 +824,14 @@ export class ProviderCatalog {
|
|
|
595
824
|
rows.sort((a, b) => rank(a) - rank(b) || (str(a.id) < str(b.id) ? -1 : str(a.id) > str(b.id) ? 1 : 0));
|
|
596
825
|
for (const row of rows)
|
|
597
826
|
row.models = stampModelEntries(row.models, str(row.id));
|
|
598
|
-
const
|
|
599
|
-
this.providersCache.set(profileHome,
|
|
600
|
-
return
|
|
827
|
+
const entry = { at: this.deps.now(), key: cacheKey, payload: { providers: rows, active_provider: active }, pooled: pooledIds };
|
|
828
|
+
this.providersCache.set(profileHome, entry);
|
|
829
|
+
return entry;
|
|
601
830
|
}
|
|
602
831
|
/** Python `get_available_models` (static catalog + live ids for keyed providers). */
|
|
603
832
|
async models(profileHome) {
|
|
833
|
+
const source = this.sourceFingerprint(profileHome);
|
|
834
|
+
const unconfirmed = new Set();
|
|
604
835
|
const config = await this.deps.config.read(profileHome);
|
|
605
836
|
const envValues = loadEnvFile(join(profileHome, '.env'));
|
|
606
837
|
const active = activeProviderFromConfig(config);
|
|
@@ -644,11 +875,11 @@ export class ProviderCatalog {
|
|
|
644
875
|
detected.add(canonical);
|
|
645
876
|
}
|
|
646
877
|
// 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.
|
|
878
|
+
// ready plugin providers (TAL-288), under their own name, and providers keyed only through the credential pool (TAL-638).
|
|
648
879
|
const signedIn = new Set();
|
|
649
880
|
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))
|
|
881
|
+
for (const row of (await this.providerRows(profileHome)).payload.providers) {
|
|
882
|
+
if (row.has_key !== true || (row.is_oauth !== true && row.is_plugin_provider !== true && row.key_source !== 'credential_pool'))
|
|
652
883
|
continue;
|
|
653
884
|
signedIn.add(str(row.id));
|
|
654
885
|
if (row.is_plugin_provider === true)
|
|
@@ -708,12 +939,15 @@ export class ProviderCatalog {
|
|
|
708
939
|
let raw = [];
|
|
709
940
|
if ('models' in providerCfg && providerCfg.models_discovered !== true)
|
|
710
941
|
raw = configuredModelOptions(providerCfg.models);
|
|
711
|
-
if (!raw.length && (this.providerHasKey(pid, config, envValues, profileHome) || signedIn.has(pid))) {
|
|
942
|
+
if (!raw.length && (this.providerHasKey(pid, config, envValues, profileHome) || selfHostedEndpoint(pid, config) || signedIn.has(pid))) {
|
|
712
943
|
const live = await this.liveModelIds(profileHome, pid);
|
|
944
|
+
// TAL-542: a failed lookup leaves this group's list unconfirmed, so the stale-model repair never trusts it.
|
|
945
|
+
if (this.liveFailed.has(`${profileHome}\0${pid}\0${source}`))
|
|
946
|
+
unconfirmed.add(pid);
|
|
713
947
|
if (live.length)
|
|
714
948
|
raw = live.map((id) => ({ id, label: pid === 'nous' ? `${formatOllamaLabel(id.includes('/') ? id.slice(id.indexOf('/') + 1) : id)} (via Nous)` : labelForModel(id, []) }));
|
|
715
949
|
// 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}`))
|
|
950
|
+
else if (pid === 'nous' && !this.liveFailed.has(`${profileHome}\0${pid}\0${source}`))
|
|
717
951
|
continue;
|
|
718
952
|
}
|
|
719
953
|
if (!raw.length)
|
|
@@ -772,15 +1006,16 @@ export class ProviderCatalog {
|
|
|
772
1006
|
const defaults = { active_provider: active, default_model: defaultModel, default_provider_id: defaultProvider, default_bare_id: defaultBare };
|
|
773
1007
|
if (!kept.length && defaultModel) {
|
|
774
1008
|
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: {} });
|
|
1009
|
+
return this.remember(profileHome, { source, unconfirmed }, { ...defaults, groups: [{ provider: 'Default', provider_id: providerId, models: stampModelEntries([{ id: defaultModel, label: labelForModel(defaultModel, []) }], providerId) }], aliases: {}, configured_model_badges: {} });
|
|
776
1010
|
}
|
|
777
1011
|
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) });
|
|
1012
|
+
return this.remember(profileHome, { source, unconfirmed }, { ...defaults, groups: stamped, aliases, configured_model_badges: this.badges(kept, active, defaultModel, fallbackCfg) });
|
|
779
1013
|
}
|
|
780
1014
|
/** Stamps the default's entry id and keeps the catalog for `modelOptionFor`. */
|
|
781
|
-
remember(profileHome, catalog) {
|
|
1015
|
+
remember(profileHome, meta, catalog) {
|
|
782
1016
|
catalog.default_option_id = catalogOptionId(catalog, catalog.default_bare_id ?? null, catalog.default_provider_id ?? null);
|
|
783
1017
|
this.lastModels.set(profileHome, catalog);
|
|
1018
|
+
this.lastModelsMeta.set(profileHome, { ...meta, at: this.deps.now() });
|
|
784
1019
|
return structuredClone(catalog);
|
|
785
1020
|
}
|
|
786
1021
|
/** Python `_configured_model_badges_from_static_catalog`: which picker rows are the main/fallback selections. */
|
|
@@ -905,11 +1140,22 @@ export class ProviderCatalog {
|
|
|
905
1140
|
return v;
|
|
906
1141
|
}
|
|
907
1142
|
}
|
|
908
|
-
|
|
909
|
-
|
|
1143
|
+
// The same config sources `providerHasKey` counts, so a provider shown as keyed resolves its key.
|
|
1144
|
+
const model = modelSection(config);
|
|
1145
|
+
if (canonicaliseProviderId(model.provider) === canonicaliseProviderId(pid) && valueCountsAsApiKey(pid, model.api_key))
|
|
1146
|
+
return str(model.api_key).trim();
|
|
1147
|
+
for (const [key, value] of Object.entries(dict(config.providers))) {
|
|
1148
|
+
if (providerIdentity(key) === providerIdentity(pid) && isDict(value) && valueCountsAsApiKey(pid, value.api_key))
|
|
1149
|
+
return str(value.api_key).trim();
|
|
1150
|
+
}
|
|
1151
|
+
return null;
|
|
910
1152
|
}
|
|
911
|
-
/**
|
|
1153
|
+
/** Account-usage snapshots by profile home, provider, and pool account. */
|
|
912
1154
|
accountUsageCache = new Map();
|
|
1155
|
+
/**
|
|
1156
|
+
* Python `get_provider_quota`. `credential` scopes the read to one credential-pool account (TAL-548): a benched account
|
|
1157
|
+
* answers with its local pool state, and every other read uses that account's key.
|
|
1158
|
+
*/
|
|
913
1159
|
async quota(profileHome, providerRaw, opts = {}) {
|
|
914
1160
|
const at = opts.at ?? this.deps.now();
|
|
915
1161
|
const computed_at = isoAt(at);
|
|
@@ -918,17 +1164,23 @@ export class ProviderCatalog {
|
|
|
918
1164
|
if (!provider)
|
|
919
1165
|
return { computed_at, ok: false, provider: null, display_name: null, supported: false, status: 'unavailable', quota: null, message: 'No active provider is configured.' };
|
|
920
1166
|
const name = displayName(provider);
|
|
1167
|
+
const credential = opts.credential ?? null;
|
|
1168
|
+
const answer = (ok, status, message, extra = {}) => ({ computed_at, ok, provider, display_name: name, supported: true, status, quota: null, ...extra, message });
|
|
1169
|
+
if (credential && credential.status !== 'available') {
|
|
1170
|
+
return answer(false, 'unavailable', `${name} credential is unavailable. ${str(credential.unavailable_reason)}`.trim(), { account_limits: poolEntryLimits(credential, at) });
|
|
1171
|
+
}
|
|
921
1172
|
if (ACCOUNT_USAGE_PROVIDERS.has(provider)) {
|
|
922
1173
|
const sidecar = this.deps.sidecar();
|
|
923
1174
|
let limits = null;
|
|
924
1175
|
// Python `_ACCOUNT_USAGE_CACHE_TTL_SECONDS`: a snapshot answers repeat polls for 45 s unless `refresh` is set.
|
|
925
|
-
const cacheKey = `${profileHome}\0${provider}`;
|
|
1176
|
+
const cacheKey = `${profileHome}\0${provider}\0${credential?.credential_id ?? ''}`;
|
|
926
1177
|
const cached = this.accountUsageCache.get(cacheKey);
|
|
927
1178
|
if (cached && !opts.refresh && this.deps.now() - cached.at <= ACCOUNT_USAGE_CACHE_TTL_S)
|
|
928
1179
|
limits = cached.limits;
|
|
929
1180
|
else if (sidecar) {
|
|
930
1181
|
try {
|
|
931
|
-
const
|
|
1182
|
+
const params = { profile_home: profileHome, provider, ...(opts.refresh ? { refresh: true } : {}), ...(credential ? { credential_id: credential.credential_id } : {}) };
|
|
1183
|
+
const snapshot = (await sidecar.call('usage.account', params, { timeoutMs: 35_000 })).snapshot;
|
|
932
1184
|
if (snapshot)
|
|
933
1185
|
limits = { ...snapshot, title: str(snapshot.title) || 'Account limits', available: snapshot.available && !str(snapshot.unavailable_reason) };
|
|
934
1186
|
}
|
|
@@ -941,40 +1193,98 @@ export class ProviderCatalog {
|
|
|
941
1193
|
if (limits)
|
|
942
1194
|
limits = { ...limits, ...normalizeQuotaWindows(limits.windows, at), fetched_at: isoUtc(limits.fetched_at) };
|
|
943
1195
|
if (limits?.available)
|
|
944
|
-
return
|
|
1196
|
+
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
1197
|
const reason = str(limits?.unavailable_reason).trim();
|
|
946
|
-
return
|
|
1198
|
+
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 });
|
|
947
1199
|
}
|
|
948
|
-
if (provider === 'openrouter') {
|
|
949
|
-
const apiKey = this.apiKeyFor(
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
if (
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1200
|
+
if (provider === 'openrouter' || provider === 'deepseek' || provider === 'opencode-go') {
|
|
1201
|
+
const apiKey = this.apiKeyFor(provider, profileHome, config);
|
|
1202
|
+
const key = credential ? { credential_id: credential.credential_id } : apiKey ? { api_key: apiKey } : null;
|
|
1203
|
+
const read = key ? await this.balance(profileHome, provider, key) : null;
|
|
1204
|
+
const status = !key || read?.status === 'no_key' ? 'no_key' : read?.status === 'ok' ? 'ok' : read?.status === 'http_error' ? read.http_status : null;
|
|
1205
|
+
if (provider === 'openrouter') {
|
|
1206
|
+
if (status === 'no_key')
|
|
1207
|
+
return answer(false, 'no_key', 'OpenRouter quota status needs an OPENROUTER_API_KEY configured on the server.');
|
|
1208
|
+
if (status === 'ok' && read?.quota) {
|
|
1209
|
+
// The cost chart follows the configured key, so another pool account's spend never lands in it.
|
|
1210
|
+
if (!credential || credential.matches_api_key)
|
|
1211
|
+
this.recordCostSnapshot(profileHome, read.quota, at);
|
|
1212
|
+
return answer(true, 'available', 'OpenRouter quota status loaded.', { label: 'OpenRouter credits', quota: read.quota });
|
|
1213
|
+
}
|
|
1214
|
+
return status === 401 || status === 403 ? answer(false, 'invalid_key', 'OpenRouter rejected the configured API key.') : answer(false, 'unavailable', 'OpenRouter quota status is temporarily unavailable.');
|
|
1215
|
+
}
|
|
1216
|
+
if (provider === 'deepseek') {
|
|
1217
|
+
if (status === 'no_key')
|
|
1218
|
+
return answer(false, 'no_key', 'DeepSeek balance needs a DEEPSEEK_API_KEY configured on the server.', { balances: [] });
|
|
1219
|
+
if (status === 'ok' && read) {
|
|
1220
|
+
const available = read.is_available === true;
|
|
1221
|
+
return answer(available, available ? 'available' : 'exhausted', available ? 'DeepSeek balance loaded.' : 'DeepSeek balance is exhausted.', { label: 'DeepSeek balance', balances: read.balances });
|
|
1222
|
+
}
|
|
1223
|
+
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: [] });
|
|
1224
|
+
}
|
|
1225
|
+
if (status === 'no_key')
|
|
1226
|
+
return answer(false, 'no_key', 'OpenCode Go account limits need an API key configured on the server.', { account_limits: null });
|
|
1227
|
+
if (status === 'ok' && read) {
|
|
1228
|
+
// Python `_sanitize_opencode_go_account_limits`: the rolling, weekly and monthly windows of the Go plan.
|
|
1229
|
+
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 }));
|
|
1230
|
+
const limits = { provider, source: 'usage_api', title: 'Account limits', plan: 'Go', details: [], available: true, unavailable_reason: null, fetched_at: isoAt(at), ...normalizeQuotaWindows(windows, at) };
|
|
1231
|
+
return answer(true, 'available', 'OpenCode Go account limits loaded.', { label: 'OpenCode Go usage', account_limits: limits });
|
|
1232
|
+
}
|
|
1233
|
+
if (status === 401)
|
|
1234
|
+
return answer(false, 'invalid_key', 'OpenCode Go rejected the configured API key.', { account_limits: null });
|
|
1235
|
+
return answer(false, 'unavailable', status === 403 ? 'OpenCode Go account limits require an active subscription.' : 'OpenCode Go account limits are temporarily unavailable.', { account_limits: null });
|
|
1236
|
+
}
|
|
1237
|
+
// Python `_local_pool_snapshot`: any other provider with a credential pool shows its accounts' local state.
|
|
1238
|
+
const entries = credential ? [credential] : (await this.poolEntries(profileHome, provider, null)) ?? [];
|
|
1239
|
+
if (entries.length) {
|
|
1240
|
+
const limits = credential ? poolEntryLimits(credential, at) : poolLimits(entries, at);
|
|
1241
|
+
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
1242
|
}
|
|
958
1243
|
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
1244
|
}
|
|
960
|
-
|
|
961
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* TAL-548: the provider's credential-pool accounts as the sidecar reads them locally. Each says whether it holds
|
|
1247
|
+
* `apiKey`, the provider's configured key. Null when the pool is unknown (see `poolLookup`).
|
|
1248
|
+
*/
|
|
1249
|
+
poolEntries(profileHome, pid, apiKey) {
|
|
1250
|
+
return this.poolLookup(profileHome, `pool\0${pid}`, async (sidecar) => (await sidecar.call('usage.pool', { profile_home: profileHome, provider: pid, ...(apiKey ? { api_key: apiKey } : {}) })).entries);
|
|
1251
|
+
}
|
|
1252
|
+
/** The last answer of each credential-pool lookup, by profile home and lookup. */
|
|
1253
|
+
lastPoolAnswers = new Map();
|
|
1254
|
+
/**
|
|
1255
|
+
* TAL-548: one credential-pool lookup through the sidecar. A failed lookup answers with the last answer to the same
|
|
1256
|
+
* lookup, else null: an unknown pool, never an empty one, so no pool account is ever reported as removed.
|
|
1257
|
+
*/
|
|
1258
|
+
async poolLookup(profileHome, lookup, call) {
|
|
1259
|
+
const key = `${profileHome}\0${lookup}`;
|
|
962
1260
|
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 };
|
|
1261
|
+
const sidecar = this.deps.sidecar();
|
|
1262
|
+
if (!sidecar)
|
|
1263
|
+
throw new Error('the sidecar is not running');
|
|
1264
|
+
const answer = await call(sidecar);
|
|
1265
|
+
this.lastPoolAnswers.set(key, answer);
|
|
1266
|
+
return answer;
|
|
975
1267
|
}
|
|
976
|
-
catch {
|
|
977
|
-
|
|
1268
|
+
catch (error) {
|
|
1269
|
+
this.deps.log(`[catalog] credential pool lookup ${lookup.replace('\0', ' ')} failed: ${str(error.message)}`);
|
|
1270
|
+
return this.lastPoolAnswers.get(key) ?? null;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
/** TAL-548: the providers whose persisted credential pool holds an added account; none when the sidecar cannot say. */
|
|
1274
|
+
poolProviders(profileHome) {
|
|
1275
|
+
return this.poolLookup(profileHome, 'providers', async (sidecar) => (await sidecar.call('usage.pool_providers', { profile_home: profileHome })).providers);
|
|
1276
|
+
}
|
|
1277
|
+
/** TAL-548: one key-based balance read through the sidecar, which resolves a pool account's key itself; null when it fails. */
|
|
1278
|
+
async balance(profileHome, provider, key) {
|
|
1279
|
+
const sidecar = this.deps.sidecar();
|
|
1280
|
+
if (!sidecar)
|
|
1281
|
+
return null;
|
|
1282
|
+
try {
|
|
1283
|
+
return await sidecar.call('usage.balance', { profile_home: profileHome, provider, ...key }, { timeoutMs: BALANCE_TIMEOUT_MS });
|
|
1284
|
+
}
|
|
1285
|
+
catch (error) {
|
|
1286
|
+
this.deps.log(`[catalog] ${provider} balance failed: ${str(error.message)}`);
|
|
1287
|
+
return null;
|
|
978
1288
|
}
|
|
979
1289
|
}
|
|
980
1290
|
quotaScopeCache = null;
|
|
@@ -1010,31 +1320,52 @@ export class ProviderCatalog {
|
|
|
1010
1320
|
return `qscope_${createHash('sha256').update(`${this.quotaServerScopeId()}\0${profile}`).digest('hex').slice(0, 32)}`;
|
|
1011
1321
|
}
|
|
1012
1322
|
/**
|
|
1013
|
-
* Python `get_provider_quotas`:
|
|
1014
|
-
*
|
|
1323
|
+
* Python `get_provider_quotas`: the stable-identity envelope the iOS widget persists. A keyed provider is one source per
|
|
1324
|
+
* credential-pool account, else one for its own key (Python `_quota_source_descriptors`, TAL-548). Each source id appears
|
|
1325
|
+
* once (two custom providers can share a slug), ordered by provider, account label, then source id.
|
|
1015
1326
|
*/
|
|
1016
1327
|
async quotas(profileHome, profile, opts = {}) {
|
|
1017
1328
|
const at = this.deps.now();
|
|
1018
|
-
const status = await this.
|
|
1329
|
+
const { payload: status, pooled: pooledIds } = await this.providerRows(profileHome);
|
|
1019
1330
|
const active = status.active_provider;
|
|
1020
1331
|
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
|
-
|
|
1332
|
+
// Python `_quota_source_id(profile, provider, credential_id)`: a pool account's id, or "provider" for the provider's own key.
|
|
1333
|
+
const sourceId = (pid, credential) => `qsrc_${createHash('sha256').update(`${scopeId}\0${pid}\0${credential}`).digest('hex').slice(0, 32)}`;
|
|
1334
|
+
// A provider configured only through `hermes auth add` has no key in .env or config.yaml, yet its pool accounts are
|
|
1335
|
+
// sources, including a signed-out OAuth provider's. An unknown pool (a failed lookup with no earlier answer) never
|
|
1336
|
+
// reports a requested source as missing.
|
|
1337
|
+
let poolUnknown = pooledIds === null;
|
|
1338
|
+
const pooled = new Set(pooledIds ?? []);
|
|
1339
|
+
const config = await this.deps.config.read(profileHome);
|
|
1340
|
+
const perProvider = await Promise.all(status.providers.filter((p) => p.has_key || p.is_custom || pooled.has(str(p.id))).map(async (p) => {
|
|
1341
|
+
const pid = str(p.id);
|
|
1342
|
+
const label = str(p.display_name) || pid;
|
|
1343
|
+
const apiKey = this.apiKeyFor(pid, profileHome, config);
|
|
1344
|
+
const found = await this.poolEntries(profileHome, pid, apiKey);
|
|
1345
|
+
if (!found)
|
|
1346
|
+
poolUnknown = true;
|
|
1347
|
+
const entries = found ?? [];
|
|
1348
|
+
const accounts = entries.map((e) => ({ source_id: sourceId(pid, e.credential_id), provider_id: pid, provider_label: label, account_label: e.label, credential: e }));
|
|
1349
|
+
// The provider's own key is a source unless a pool account already holds it.
|
|
1350
|
+
const ownKey = !entries.length || (apiKey !== null && !entries.some((e) => e.matches_api_key));
|
|
1351
|
+
return ownKey ? [{ source_id: sourceId(pid, 'provider'), provider_id: pid, provider_label: label, account_label: label, credential: null }, ...accounts] : accounts;
|
|
1352
|
+
}));
|
|
1353
|
+
let descriptors = uniqueQuotaSources(perProvider.flat());
|
|
1024
1354
|
const requested = str(opts.sourceId).trim() || null;
|
|
1025
1355
|
if (requested)
|
|
1026
1356
|
descriptors = descriptors.filter((d) => d.source_id === requested);
|
|
1357
|
+
const thresholds = this.deps.quotaThresholds?.(profile) ?? QUOTA_THRESHOLD_DEFAULTS;
|
|
1027
1358
|
const sources = await Promise.all(descriptors.map(async (d) => {
|
|
1028
|
-
const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at });
|
|
1359
|
+
const q = await this.quota(profileHome, d.provider_id, { refresh: opts.refresh ?? false, at, credential: d.credential });
|
|
1029
1360
|
const limits = dict(q.account_limits);
|
|
1030
|
-
return {
|
|
1361
|
+
return classifyQuotaSource({
|
|
1031
1362
|
source_id: d.source_id, provider_id: d.provider_id, provider_label: d.provider_label, account_label: d.account_label,
|
|
1032
1363
|
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 ?? [],
|
|
1364
|
+
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
1365
|
unavailable_reason: limits.unavailable_reason ?? null, retry_after: limits.retry_after ?? null, fetched_at: limits.fetched_at ?? null, message: q.message ?? null,
|
|
1035
|
-
};
|
|
1366
|
+
}, thresholds);
|
|
1036
1367
|
}));
|
|
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 };
|
|
1368
|
+
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
1369
|
}
|
|
1039
1370
|
/** Python `get_provider_cost_history` (OpenRouter only; daily snapshots under `<home>/cost-snapshots`). */
|
|
1040
1371
|
async costHistory(profileHome, providerRaw, days) {
|
|
@@ -1049,56 +1380,46 @@ export class ProviderCatalog {
|
|
|
1049
1380
|
const apiKey = this.apiKeyFor('openrouter', profileHome, config);
|
|
1050
1381
|
if (!apiKey)
|
|
1051
1382
|
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.' };
|
|
1383
|
+
const info = await this.balance(profileHome, 'openrouter', { api_key: apiKey });
|
|
1384
|
+
if (info?.status !== 'ok' || !info.quota) {
|
|
1385
|
+
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
1386
|
}
|
|
1080
|
-
|
|
1387
|
+
const snapshots = this.recordCostSnapshot(profileHome, info.quota, this.deps.now());
|
|
1388
|
+
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.' };
|
|
1389
|
+
}
|
|
1390
|
+
readCostSnapshots(profileHome) {
|
|
1391
|
+
try {
|
|
1392
|
+
const data = JSON.parse(readFileSync(join(profileHome, 'cost-snapshots', 'openrouter.json'), 'utf8'));
|
|
1393
|
+
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));
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
return [];
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/** Writes or updates the UTC day's OpenRouter usage snapshot; every successful key read (quota or cost history) records one. */
|
|
1400
|
+
recordCostSnapshot(profileHome, quota, at) {
|
|
1401
|
+
let snapshots = this.readCostSnapshots(profileHome);
|
|
1081
1402
|
try {
|
|
1082
|
-
const today =
|
|
1083
|
-
const
|
|
1084
|
-
const limit =
|
|
1403
|
+
const today = isoAt(at).slice(0, 10);
|
|
1404
|
+
const used = finite(quota.usage);
|
|
1405
|
+
const limit = finite(quota.limit);
|
|
1085
1406
|
const existing = snapshots.find((e) => e.date === today);
|
|
1086
1407
|
if (existing) {
|
|
1087
|
-
existing.used =
|
|
1408
|
+
existing.used = used;
|
|
1088
1409
|
existing.limit = limit;
|
|
1089
1410
|
}
|
|
1090
1411
|
else
|
|
1091
|
-
snapshots.push({ date: today, used
|
|
1412
|
+
snapshots.push({ date: today, used, limit });
|
|
1092
1413
|
snapshots.sort((a, b) => (a.date < b.date ? -1 : 1));
|
|
1093
1414
|
if (snapshots.length > COST_SNAPSHOT_MAX_DAYS)
|
|
1094
1415
|
snapshots = snapshots.slice(-COST_SNAPSHOT_MAX_DAYS);
|
|
1095
1416
|
mkdirSync(join(profileHome, 'cost-snapshots'), { recursive: true });
|
|
1096
|
-
atomicWriteText(
|
|
1417
|
+
atomicWriteText(join(profileHome, 'cost-snapshots', 'openrouter.json'), JSON.stringify({ provider: 'openrouter', snapshots }, null, 2));
|
|
1418
|
+
return snapshots;
|
|
1097
1419
|
}
|
|
1098
1420
|
catch {
|
|
1099
|
-
|
|
1421
|
+
return this.readCostSnapshots(profileHome);
|
|
1100
1422
|
}
|
|
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
1423
|
}
|
|
1103
1424
|
}
|
|
1104
1425
|
export const envFileExists = (profileHome) => existsSync(join(profileHome, '.env'));
|