@pheem49/mint 1.17.0 → 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/Cargo.lock +6 -6
- package/Cargo.toml +2 -2
- package/README.md +8 -2
- package/Release_Note.md +222 -71
- package/crates/mint-cli/src/agent/diff_render.rs +66 -44
- package/crates/mint-cli/src/agent/live_status.rs +21 -4
- package/crates/mint-cli/src/agent/markdown_render.rs +342 -5
- package/crates/mint-cli/src/agent.rs +25 -10
- package/crates/mint-cli/src/commands/agent.rs +4 -92
- package/crates/mint-cli/src/commands/browser.rs +2 -15
- package/crates/mint-cli/src/commands/integrations.rs +3 -3
- package/crates/mint-cli/src/commands/mod.rs +0 -3
- package/crates/mint-cli/src/commands/tailnet_web.rs +1 -0
- package/crates/mint-cli/src/interactive/chat_tui/dialog.rs +572 -0
- package/crates/mint-cli/src/interactive/chat_tui/mod.rs +1427 -0
- package/crates/mint-cli/src/interactive/chat_tui/render.rs +1524 -0
- package/crates/mint-cli/src/interactive/chat_tui/selection.rs +401 -0
- package/crates/mint-cli/src/interactive/chat_tui/state.rs +1833 -0
- package/crates/mint-cli/src/interactive/chat_tui/tests.rs +3414 -0
- package/crates/mint-cli/src/interactive/chat_tui/thought.rs +202 -0
- package/crates/mint-cli/src/interactive/chat_tui/transcript.rs +728 -0
- package/crates/mint-cli/src/interactive/chat_tui/watermark.rs +148 -0
- package/crates/mint-cli/src/interactive/command_ui.rs +53 -5
- package/crates/mint-cli/src/interactive/composer.rs +186 -0
- package/crates/mint-cli/src/interactive/format.rs +78 -3
- package/crates/mint-cli/src/interactive/input_box.rs +61 -1
- package/crates/mint-cli/src/interactive/resume_picker.rs +367 -25
- package/crates/mint-cli/src/interactive/slash_commands.rs +648 -371
- package/crates/mint-cli/src/interactive/tool_activity.rs +93 -20
- package/crates/mint-cli/src/interactive.rs +37 -0
- package/crates/mint-cli/src/main.rs +6 -3
- package/crates/mint-cli/src/markdown.rs +2 -37
- package/crates/mint-cli/src/onboard.rs +700 -282
- package/crates/mint-cli/src/setup.rs +1342 -624
- package/crates/mint-cli/src/terminal_theme.rs +159 -35
- package/crates/mint-cli/tests/library_intake.rs +1 -0
- package/crates/mint-core/src/agent/chat/anthropic.rs +318 -0
- package/crates/mint-core/src/agent/chat/custom.rs +149 -0
- package/crates/mint-core/src/agent/chat/gemini.rs +518 -0
- package/crates/mint-core/src/agent/chat/mod.rs +857 -0
- package/crates/mint-core/src/agent/chat/ollama.rs +321 -0
- package/crates/mint-core/src/agent/chat/openai.rs +582 -0
- package/crates/mint-core/src/agent/chat/streaming.rs +469 -0
- package/crates/mint-core/src/agent/chat/tests.rs +1479 -0
- package/crates/mint-core/src/agent/memory.rs +56 -2
- package/crates/mint-core/src/agent/tasks.rs +2 -2
- package/crates/mint-core/src/api_server/routes/chat.rs +24 -2
- package/crates/mint-core/src/api_server/routes/gemini_live.rs +14 -1
- package/crates/mint-core/src/api_server/routes/html_preview.rs +1 -8
- package/crates/mint-core/src/api_server/routes/sessions.rs +21 -0
- package/crates/mint-core/src/avatar_bridge.rs +0 -22
- package/crates/mint-core/src/browser/cdp.rs +10 -4
- package/crates/mint-core/src/browser/headless.rs +582 -0
- package/crates/mint-core/src/browser/input.rs +7 -62
- package/crates/mint-core/src/browser/interact.rs +0 -14
- package/crates/mint-core/src/browser/mod.rs +8 -29
- package/crates/mint-core/src/browser/navigate.rs +2 -36
- package/crates/mint-core/src/browser/runtime.rs +2 -4
- package/crates/mint-core/src/browser/session.rs +50 -466
- package/crates/mint-core/src/browser/workflow.rs +142 -32
- package/crates/mint-core/src/cron/store.rs +2 -2
- package/crates/mint-core/src/integrations/mcp.rs +6 -8
- package/crates/mint-core/src/lib.rs +0 -5
- package/crates/mint-core/src/media/gemini_live.rs +636 -86
- package/crates/mint-core/src/media/library/migration.rs +26 -0
- package/crates/mint-core/src/media/library/tests.rs +1 -1
- package/crates/mint-core/src/media/library/thumbnails.rs +8 -2
- package/crates/mint-core/src/media/pictures.rs +1 -1
- package/crates/mint-core/src/media/tool_images.rs +92 -1
- package/crates/mint-core/src/mods/manager.rs +101 -35
- package/crates/mint-core/src/orchestration/context_render.rs +441 -24
- package/crates/mint-core/src/orchestration/decision_parsing.rs +531 -16
- package/crates/mint-core/src/orchestration/full_skill_result_tests.rs +12 -0
- package/crates/mint-core/src/orchestration/loop_guard.rs +145 -0
- package/crates/mint-core/src/orchestration/mcp_outcome_tests.rs +864 -0
- package/crates/mint-core/src/orchestration/memory_skill.rs +88 -28
- package/crates/mint-core/src/orchestration/mod.rs +320 -4249
- package/crates/mint-core/src/orchestration/reliability.rs +67 -27
- package/crates/mint-core/src/orchestration/review_regressions.rs +5 -0
- package/crates/mint-core/src/orchestration/tests.rs +1665 -0
- package/crates/mint-core/src/orchestration/tools/dispatch.rs +967 -0
- package/crates/mint-core/src/orchestration/tools/files.rs +1 -0
- package/crates/mint-core/src/orchestration/tools/mod.rs +5 -1
- package/crates/mint-core/src/orchestration/tools/planning.rs +102 -42
- package/crates/mint-core/src/orchestration/tools/shell.rs +129 -58
- package/crates/mint-core/src/orchestration/truncation.rs +170 -13
- package/crates/mint-core/src/orchestration/types.rs +648 -0
- package/crates/mint-core/src/orchestration/verification.rs +0 -3
- package/crates/mint-core/src/orchestration/workspace_helpers.rs +24 -6
- package/crates/mint-core/src/prompts/agent.rs +52 -76
- package/crates/mint-core/src/prompts/tool_catalog.rs +8 -151
- package/crates/mint-core/src/search/files.rs +20 -0
- package/crates/mint-core/src/slash/mod.rs +228 -265
- package/crates/mint-core/src/slash/models.rs +6 -1
- package/crates/mint-core/src/system/bg_shell.rs +1 -0
- package/crates/mint-core/src/system/config.rs +176 -34
- package/crates/mint-mod-runtime/src/lib.rs +37 -23
- package/out/web/assets/ArtifactPreviewPanel-Cx6BVOOF.js +2 -0
- package/out/web/assets/CalculationCard-D2E74u5q.js +1 -0
- package/out/web/assets/CliSessionsView-ZbdtSjvK.css +1 -0
- package/out/web/assets/{CliSessionsView-MI8kXMAb.js → CliSessionsView-z5i6qGyb.js} +1 -1
- package/out/web/assets/CodeReviewPage-BK2bOScd.css +1 -0
- package/out/web/assets/CodeReviewPage-Dp6zHuHu.js +2 -0
- package/out/web/assets/{CommandPalette-BD8SlTE5.js → CommandPalette-DaKlzbKs.js} +1 -1
- package/out/web/assets/ImageGenCard-BMCJHP9O.js +1 -0
- package/out/web/assets/ImageSearchCard-Cld_SOUg.js +1 -0
- package/out/web/assets/{ImageStudioPanel-BP7N4rLr.js → ImageStudioPanel-B5N7TjeB.js} +1 -1
- package/out/web/assets/LibraryPage-CMB89rJ0.css +1 -0
- package/out/web/assets/LibraryPage-DbZEi4W_.js +2 -0
- package/out/web/assets/LinkedFoldersView-9B9DIPoZ.js +1 -0
- package/out/web/assets/{McpRegistryPicker-CoS83vtc.js → McpRegistryPicker-DnXPlzI7.js} +1 -1
- package/out/web/assets/McpServersView-CiNBNUl4.js +1 -0
- package/out/web/assets/MermaidCard-BBDNEXbY.js +2 -0
- package/out/web/assets/MintDashboard-C_UYPoNV.js +119 -0
- package/out/web/assets/MintDashboard-DTd8C_c8.css +1 -0
- package/out/web/assets/PlanCard-C2x5pajq.js +1 -0
- package/out/web/assets/{PluginsView-jBYmcKh4.js → PluginsView-DoTg9MVa.js} +1 -1
- package/out/web/assets/RewindModal-B5oowrR0.js +1 -0
- package/out/web/assets/{ScheduledTasksView-5uw9-V9H.js → ScheduledTasksView-BD5DVPkZ.js} +1 -1
- package/out/web/assets/{SelectField-DXBQtrUK.js → SelectField-Cy6CghrU.js} +1 -1
- package/out/web/assets/SettingsWindow-BYl_oCFh.js +4 -0
- package/out/web/assets/SettingsWindow-Ckcgnjt8.css +1 -0
- package/out/web/assets/{SkillsView-C-gp9J7m.js → SkillsView-CIeWwq5z.js} +1 -1
- package/out/web/assets/StockCard-DxUciKyp.js +1 -0
- package/out/web/assets/ToolSurfacePage-qBgs_VHq.css +1 -0
- package/out/web/assets/ToolSurfacePage-saVbVrkE.js +38 -0
- package/out/web/assets/{UiFeatureCard-6BwxTOCe.js → UiFeatureCard-CdY0hGWy.js} +1 -1
- package/out/web/assets/{UiGridCard-DvZcLNxb.js → UiGridCard-BBK3I5hC.js} +1 -1
- package/out/web/assets/UiMockupWidget-CpR9EDuc.js +1 -0
- package/out/web/assets/{VeoStudioPanel-DOj7_WR3.css → VeoStudioPanel-9kU9YLEN.css} +1 -1
- package/out/web/assets/{VeoStudioPanel-DfsOJKv0.js → VeoStudioPanel-Co6-wEWM.js} +2 -2
- package/out/web/assets/WeatherCard-Buzi7Vfu.js +1 -0
- package/out/web/assets/WorkspacePdfPreview-Bi8Zzyir.js +2 -0
- package/out/web/assets/{abnfDiagram-VCTEODGH-BOo3Ckng.js → abnfDiagram-VCTEODGH-CMfR95N2.js} +1 -1
- package/out/web/assets/{architecture-7GRP2DOG-ZOwheW3n.js → architecture-7GRP2DOG-DhAM4HyO.js} +1 -1
- package/out/web/assets/{architectureDiagram-5GKGNRK7-DC2FDzVd.js → architectureDiagram-5GKGNRK7-Yk3TUJ_F.js} +1 -1
- package/out/web/assets/{blockDiagram-I7D4REHJ-BrKFWdAH.js → blockDiagram-I7D4REHJ-X9fBgJik.js} +1 -1
- package/out/web/assets/{c4Diagram-7LVT6UL2-BHTHK1Yd.js → c4Diagram-7LVT6UL2-Bk9x7N-8.js} +1 -1
- package/out/web/assets/{chunk-4HAMMTFA-BYFkZwNj.js → chunk-4HAMMTFA-Dx5fk1_0.js} +1 -1
- package/out/web/assets/{chunk-75Z2AOVW-BSKfSKSA.js → chunk-75Z2AOVW-BszTQoM4.js} +1 -1
- package/out/web/assets/{chunk-DU6HZSFF-vIhsiW6n.js → chunk-DU6HZSFF-DK-As3rk.js} +1 -1
- package/out/web/assets/{chunk-F27PBJKO-BlYpW9xq.js → chunk-F27PBJKO-LmncZZIS.js} +1 -1
- package/out/web/assets/{chunk-GMAD6QVW-BIBz_Ug5.js → chunk-GMAD6QVW-DSQ_sDRJ.js} +1 -1
- package/out/web/assets/{chunk-GVQU2GXP-NPSHxeqC.js → chunk-GVQU2GXP-Cno3ulje.js} +1 -1
- package/out/web/assets/{chunk-IMKFNOWR-YlJK8_Fp.js → chunk-IMKFNOWR-bIzfY9Jc.js} +1 -1
- package/out/web/assets/{chunk-L3NEJ4N5-DMYi4-af.js → chunk-L3NEJ4N5-Dl6eOa0D.js} +1 -1
- package/out/web/assets/{chunk-OSK3NFVY-Begd3-jA.js → chunk-OSK3NFVY-BaUGJ-cM.js} +1 -1
- package/out/web/assets/{chunk-P2QGCYS3-D1I4xFHN.js → chunk-P2QGCYS3-_066ndhH.js} +1 -1
- package/out/web/assets/{chunk-POPQ4Y6H-CafZZzNE.js → chunk-POPQ4Y6H-YT1AKhTv.js} +1 -1
- package/out/web/assets/{chunk-PWAF6VOD-Dwh6_Lnt.js → chunk-PWAF6VOD-CrEgDsxh.js} +1 -1
- package/out/web/assets/{chunk-SHT3W25Y-B0XrCSEb.js → chunk-SHT3W25Y-_KwaRLOl.js} +1 -1
- package/out/web/assets/{chunk-SVP7TREG-CxLsddxC.js → chunk-SVP7TREG-BQ1qb32L.js} +1 -1
- package/out/web/assets/{chunk-TICWLB2K-y156QInb.js → chunk-TICWLB2K-BDyehCtg.js} +1 -1
- package/out/web/assets/classDiagram-ZZMXUADV-BoSpzRCJ.js +1 -0
- package/out/web/assets/classDiagram-v2-VYDZK3BY-BoSpzRCJ.js +1 -0
- package/out/web/assets/{config-DqXJXeZM.css → config-2suVFPZg.css} +1 -1
- package/out/web/assets/config-CkRCdkxn.js +2 -0
- package/out/web/assets/{cynefin-OW5HDTMX-SMVvrLf-.js → cynefin-OW5HDTMX-Dl_31aaN.js} +1 -1
- package/out/web/assets/{cynefinDiagram-5FMLGOSQ-rZznN24T.js → cynefinDiagram-5FMLGOSQ-Bpir8jha.js} +1 -1
- package/out/web/assets/{dagre-GXQ25YYZ-BOToydFu.js → dagre-GXQ25YYZ-D9xu3nsq.js} +1 -1
- package/out/web/assets/{diagram-S7CK7UJ4-TMVaGQEJ.js → diagram-S7CK7UJ4-DeveoEeB.js} +1 -1
- package/out/web/assets/{diagram-UQ7AKVKN-C2JK-GP2.js → diagram-UQ7AKVKN-p4xWRESV.js} +1 -1
- package/out/web/assets/{diagram-VSXAHHWV-Cw8VSi2K.js → diagram-VSXAHHWV-C7qz9NDF.js} +1 -1
- package/out/web/assets/{diagram-VX7I27RA-DXp4fKk9.js → diagram-VX7I27RA-DRiCa3Vd.js} +1 -1
- package/out/web/assets/{diagram-Z3DM3KII-CzSicYiW.js → diagram-Z3DM3KII-Bt7zduQX.js} +1 -1
- package/out/web/assets/{ebnfDiagram-PWID7BFC-Dqj-zqPu.js → ebnfDiagram-PWID7BFC-DV9WpwdR.js} +1 -1
- package/out/web/assets/{erDiagram-RLTQ6QDP-DZwFZAuD.js → erDiagram-RLTQ6QDP-BJ_nwG2m.js} +1 -1
- package/out/web/assets/{eventmodeling-NTZA5JFV-C0O-Lvgp.js → eventmodeling-NTZA5JFV-BTBMrfVa.js} +1 -1
- package/out/web/assets/flowDiagram-HODETNUW-Cdlq3cJL.js +1 -0
- package/out/web/assets/{ganttDiagram-EL5Y4UJY-Da4ULXTl.js → ganttDiagram-EL5Y4UJY-DQLdsXrC.js} +1 -1
- package/out/web/assets/{gitGraph-4MIJSDKK-B9wNFZIP.js → gitGraph-4MIJSDKK-CL1OEOQ_.js} +1 -1
- package/out/web/assets/{gitGraphDiagram-WWUBYQGX-RjrhB0MP.js → gitGraphDiagram-WWUBYQGX-8cSi42H7.js} +1 -1
- package/out/web/assets/index-BocxnrDG.js +168 -0
- package/out/web/assets/index-yeh_r55a.css +1 -0
- package/out/web/assets/{info-A6RAGUB7-B3dfCpeY.js → info-A6RAGUB7-C1zsX5sc.js} +1 -1
- package/out/web/assets/{infoDiagram-27XIBGKW-QVWkLVx7.js → infoDiagram-27XIBGKW-DsRYtIyI.js} +1 -1
- package/out/web/assets/{ishikawaDiagram-5VMMS53U-CLFNIRI9.js → ishikawaDiagram-5VMMS53U-B2crwIXj.js} +1 -1
- package/out/web/assets/{journeyDiagram-3NMN7TZE-fgOQadYF.js → journeyDiagram-3NMN7TZE-BTH-GyVR.js} +1 -1
- package/out/web/assets/{kanban-definition-UXKFOSKX-G3Q_9g9O.js → kanban-definition-UXKFOSKX-BeRfCnuA.js} +1 -1
- package/out/web/assets/{mermaid-parser.core-v3Jm7nco.js → mermaid-parser.core-BJwgCBBc.js} +3 -3
- package/out/web/assets/{mermaid.core-DTEjLUA1.js → mermaid.core-CsDDodN8.js} +4 -4
- package/out/web/assets/{mindmap-definition-YA3MSWOX-C_D42_fF.js → mindmap-definition-YA3MSWOX-B42efOQ1.js} +1 -1
- package/out/web/assets/{modelManager-BPLycQrN.js → modelManager-B3ZFMh8v.js} +2 -2
- package/out/web/assets/{oauthManager-BUK7ic7q.js → oauthManager-Ch5Du2fF.js} +1 -1
- package/out/web/assets/{packet-AYTQ26CC-B-R-SHlF.js → packet-AYTQ26CC-B6xUyE6r.js} +1 -1
- package/out/web/assets/{pegDiagram-XKGWAZYB-Da2q_giz.js → pegDiagram-XKGWAZYB-1LabVycL.js} +1 -1
- package/out/web/assets/{pie-WAS4IAKB-BC8F0N7-.js → pie-WAS4IAKB-DJwo-nf_.js} +1 -1
- package/out/web/assets/{pieDiagram-E7YTZNPT-Iy9REJNC.js → pieDiagram-E7YTZNPT-icmAqzy4.js} +1 -1
- package/out/web/assets/{quadrantDiagram-AXDQQJYC-ztsiYw7h.js → quadrantDiagram-AXDQQJYC-CZ-wEx41.js} +1 -1
- package/out/web/assets/{radar-RG4KPBEZ-B_1P8Z_z.js → radar-RG4KPBEZ-D7d-Xriw.js} +1 -1
- package/out/web/assets/{railroad-74A4TZTK-DuTqczSo.js → railroad-74A4TZTK-viuhdYbs.js} +1 -1
- package/out/web/assets/{railroad-abnf-HS5TGJTU-Df29WLmp.js → railroad-abnf-HS5TGJTU-DNXiCNea.js} +1 -1
- package/out/web/assets/{railroad-ebnf-LZEXJU2U-ePFMo-qp.js → railroad-ebnf-LZEXJU2U-nkSu1-0C.js} +1 -1
- package/out/web/assets/{railroad-peg-WCYAUIDC-DYXLW4_E.js → railroad-peg-WCYAUIDC-BhbMeQzz.js} +1 -1
- package/out/web/assets/{railroadDiagram-O6MQD6OU-LStnZf2S.js → railroadDiagram-O6MQD6OU-CLpG1jUX.js} +1 -1
- package/out/web/assets/{requirementDiagram-BXWQKSXE-DmzonMH-.js → requirementDiagram-BXWQKSXE-kRDrvWrc.js} +1 -1
- package/out/web/assets/{sankeyDiagram-P5KCCOFB-B8vjmUU1.js → sankeyDiagram-P5KCCOFB-DxAwmq4x.js} +1 -1
- package/out/web/assets/{sequenceDiagram-WJ2MYXX4-CTnplOAX.js → sequenceDiagram-WJ2MYXX4-aLgY-6Wz.js} +1 -1
- package/out/web/assets/{stateDiagram-D77RDMKH-BFrIaLdI.js → stateDiagram-D77RDMKH-D5QWfOhp.js} +1 -1
- package/out/web/assets/stateDiagram-v2-MP3YSRHH-D6OiZoAP.js +1 -0
- package/out/web/assets/{swimlanes-42K2YHIH-BXGngUNU.js → swimlanes-42K2YHIH-CRs5UyA-.js} +1 -1
- package/out/web/assets/swimlanesDiagram-VR7AAH4N-UgEXIsV0.js +8 -0
- package/out/web/assets/syntaxHighlight-C-wAGT84.js +3 -0
- package/out/web/assets/{timeline-definition-24CTP7MA-CegH7n43.js → timeline-definition-24CTP7MA-Denqd9qb.js} +1 -1
- package/out/web/assets/{treeView-Q6P3EWNA-DTpJv2Tw.js → treeView-Q6P3EWNA-0my3Z3s4.js} +1 -1
- package/out/web/assets/{treemap-WGGIJYW6-i8M9iJU6.js → treemap-WGGIJYW6-B_EjkLYq.js} +1 -1
- package/out/web/assets/{vennDiagram-4TSXK5OY-Dpzo1S8U.js → vennDiagram-4TSXK5OY-DxVg3Ewr.js} +1 -1
- package/out/web/assets/{wardley-WFR3VGLG-BrNsgtfw.js → wardley-WFR3VGLG-CGD-Tj4p.js} +1 -1
- package/out/web/assets/{wardleyDiagram-VM6X3IG4-lQ92LFE1.js → wardleyDiagram-VM6X3IG4-CmfS39Xm.js} +1 -1
- package/out/web/assets/{xychartDiagram-S5SC5T6Z-BaOaEoBd.js → xychartDiagram-S5SC5T6Z-BVM9bKis.js} +1 -1
- package/out/web/index-web.html +2 -2
- package/out/web/index.html +2 -2
- package/package.json +2 -2
- package/shared-modules/CODEOWNERS +3 -3
- package/shared-modules/SDL2/SDL2_mixer.json +27 -0
- package/shared-modules/appstream/README.md +13 -0
- package/shared-modules/appstream/appstream-compose.json +111 -0
- package/shared-modules/dbus-glib/dbus-glib.json +2 -2
- package/shared-modules/libportal/libportal.json +23 -0
- package/shared-modules/linux-audio/fluidsynth2-static.json +2 -2
- package/shared-modules/linux-audio/fluidsynth2.json +2 -2
- package/shared-modules/linux-audio/libinstpatch.json +2 -2
- package/shared-modules/linux-audio/liblo-static.json +3 -0
- package/shared-modules/luajit/luajit.json +2 -2
- package/shared-modules/squashfs-tools/squashfs-tools.json +0 -3
- package/slash-commands.json +7 -2
- package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/renderer/shared/agentProgress.ts +51 -3
- package/src/renderer/shared/components/Settings/AutomationTab.tsx +2 -56
- package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +15 -1
- package/src/renderer/shared/components/Settings/GeneralTab.tsx +25 -12
- package/src/renderer/shared/components/Settings/ThemeTab.test.tsx +45 -0
- package/src/renderer/shared/components/Settings/ThemeTab.tsx +106 -3
- package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +67 -17
- package/src/renderer/shared/components/activity/AgentActivityTable.tsx +376 -134
- package/src/renderer/shared/components/activity/AskUserActivityCard.test.tsx +122 -0
- package/src/renderer/shared/components/activity/AskUserActivityCard.tsx +70 -0
- package/src/renderer/shared/components/activity/McpToolResults.tsx +39 -2
- package/src/renderer/shared/components/activity/SubagentDagView.tsx +7 -7
- package/src/renderer/shared/components/activity/ViewedImages.tsx +41 -2
- package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +3 -3
- package/src/renderer/shared/components/app/CommandPalette.tsx +7 -7
- package/src/renderer/shared/components/app/DesktopTitlebar.tsx +2 -2
- package/src/renderer/shared/components/app/MintDashboard.tsx +27 -17
- package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +3 -3
- package/src/renderer/shared/components/cards/CalculationCard.tsx +3 -3
- package/src/renderer/shared/components/cards/ImageGenCard.tsx +2 -2
- package/src/renderer/shared/components/cards/ImageSearchCard.tsx +2 -2
- package/src/renderer/shared/components/cards/MermaidCard.tsx +1 -1
- package/src/renderer/shared/components/cards/PlanCard.test.tsx +41 -0
- package/src/renderer/shared/components/cards/PlanCard.tsx +32 -0
- package/src/renderer/shared/components/cards/ReleaseNotesCard.test.tsx +29 -0
- package/src/renderer/shared/components/cards/ReleaseNotesCard.tsx +137 -0
- package/src/renderer/shared/components/cards/StockCard.tsx +7 -7
- package/src/renderer/shared/components/cards/ToolResultCards.tsx +11 -3
- package/src/renderer/shared/components/cards/UiMockupWidget.tsx +4 -4
- package/src/renderer/shared/components/cards/WeatherCard.tsx +5 -5
- package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +11 -0
- package/src/renderer/shared/components/chat/ApprovalCard.tsx +21 -2
- package/src/renderer/shared/components/chat/ArtifactCards.test.tsx +69 -0
- package/src/renderer/shared/components/chat/ArtifactCards.tsx +238 -0
- package/src/renderer/shared/components/chat/AttachedDocumentCard.test.tsx +39 -0
- package/src/renderer/shared/components/chat/AttachedDocumentCard.tsx +220 -0
- package/src/renderer/shared/components/chat/ChatCodeBlock.test.tsx +33 -0
- package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +15 -1
- package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +40 -2
- package/src/renderer/shared/components/chat/ChatMessageItem.tsx +180 -34
- package/src/renderer/shared/components/chat/ChatModeSelector.css +227 -0
- package/src/renderer/shared/components/chat/ChatModeSelector.test.tsx +45 -0
- package/src/renderer/shared/components/chat/ChatModeSelector.tsx +198 -0
- package/src/renderer/shared/components/chat/ChatPanel.test.tsx +22 -0
- package/src/renderer/shared/components/chat/ChatPanel.tsx +227 -53
- package/src/renderer/shared/components/chat/DiffReviewModal.tsx +6 -6
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +142 -1
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +45 -3
- package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +42 -14
- package/src/renderer/shared/components/chat/RewindModal.tsx +10 -10
- package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +14 -0
- package/src/renderer/shared/components/chat/SourcesBlock.test.tsx +53 -0
- package/src/renderer/shared/components/chat/SourcesBlock.tsx +236 -90
- package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +45 -1
- package/src/renderer/shared/components/chat/TurnFileChanges.tsx +202 -169
- package/src/renderer/shared/components/extensions/CliSessionsView.tsx +5 -4
- package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +3 -3
- package/src/renderer/shared/components/studio/LibraryPage.tsx +48 -5
- package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +125 -13
- package/src/renderer/shared/components/workspace/CodeReviewPage.test.tsx +50 -0
- package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +77 -16
- package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +78 -22
- package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +13 -0
- package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +48 -28
- package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +266 -47
- package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +18 -6
- package/src/renderer/shared/components/workspace/background-terminals.css +11 -11
- package/src/renderer/shared/constants/config.ts +1 -5
- package/src/renderer/shared/constants/models.test.ts +51 -0
- package/src/renderer/shared/constants/models.ts +139 -32
- package/src/renderer/shared/conversation/lifecycle.test.ts +91 -3
- package/src/renderer/shared/conversation/lifecycle.ts +104 -5
- package/src/renderer/shared/conversation/progressiveStream.test.ts +83 -0
- package/src/renderer/shared/conversation/progressiveStream.ts +219 -0
- package/src/renderer/shared/css/agent-observability.css +133 -0
- package/src/renderer/shared/css/chat-ui-cards.css +13 -13
- package/src/renderer/shared/css/code-review.css +63 -34
- package/src/renderer/shared/css/code-sessions.css +47 -18
- package/src/renderer/shared/css/collapsed-paste.css +1 -1
- package/src/renderer/shared/css/error-notice.css +1 -1
- package/src/renderer/shared/css/file-changes.css +165 -80
- package/src/renderer/shared/css/library.css +105 -10
- package/src/renderer/shared/css/management-views.css +1 -1
- package/src/renderer/shared/css/mcp-results.css +5 -5
- package/src/renderer/shared/css/model-selector.css +18 -18
- package/src/renderer/shared/css/mods.css +7 -7
- package/src/renderer/shared/css/notification-notice.css +2 -2
- package/src/renderer/shared/css/queued-messages.css +5 -5
- package/src/renderer/shared/css/settings/general.css +18 -13
- package/src/renderer/shared/css/settings/profile.css +5 -5
- package/src/renderer/shared/css/settings/theme.css +5 -5
- package/src/renderer/shared/css/slash-choice.css +2 -2
- package/src/renderer/shared/css/slash-suggestions.css +3 -3
- package/src/renderer/shared/css/sources-block.css +41 -2
- package/src/renderer/shared/css/tools-panel.css +2 -2
- package/src/renderer/shared/css/ui-tailwind.css +8 -0
- package/src/renderer/shared/css/veo-studio.css +2 -2
- package/src/renderer/shared/css/viewed-images.css +7 -7
- package/src/renderer/shared/platform.ts +2 -1
- package/src/renderer/shared/theme/themeManager.test.ts +17 -0
- package/src/renderer/shared/theme/themeManager.ts +21 -4
- package/src/renderer/shared/theme/themes.ts +54 -0
- package/src/renderer/shared/theme/tokens.css +6 -4
- package/src/renderer/shared/types.ts +1 -1
- package/src/renderer/shared/utils/agentActivity.test.ts +468 -2
- package/src/renderer/shared/utils/agentActivity.ts +602 -35
- package/src/renderer/shared/utils/artifactBrowser.test.ts +37 -32
- package/src/renderer/shared/utils/artifactBrowser.ts +17 -19
- package/src/renderer/shared/utils/attachedDocument.test.ts +73 -0
- package/src/renderer/shared/utils/attachedDocument.ts +49 -0
- package/src/renderer/shared/utils/fileIcons.test.ts +59 -0
- package/src/renderer/shared/utils/fileIcons.ts +149 -16
- package/src/renderer/shared/utils/markdown.tsx +72 -19
- package/src/renderer/shared/utils/markdown.webImages.test.tsx +16 -0
- package/src/renderer/shared/utils/mcpResults.test.tsx +29 -0
- package/src/renderer/shared/utils/slashCommandProcessor.ts +4 -1
- package/src/renderer/shared/utils/syntaxHighlight.test.tsx +11 -1
- package/src/renderer/shared/utils/syntaxHighlight.tsx +51 -3
- package/src/renderer/shared/utils/useGeminiLiveVoice.ts +324 -51
- package/src/renderer/shared/utils/workspaceDrag.test.ts +153 -0
- package/src/renderer/shared/utils/workspaceDrag.ts +142 -0
- package/src/renderer/shared/utils/workspaceMedia.ts +10 -2
- package/src/renderer/src/App.tsx +14 -8
- package/src/renderer/src/agentProgress.ts +1 -0
- package/src/renderer/src/components/ScreenPicker.css +1 -1
- package/src/renderer/src/components/WorkspacePanel.tsx +6 -5
- package/src/renderer/src/css/app-shell.css +4 -4
- package/src/renderer/src/css/chat.css +1261 -241
- package/src/renderer/src/css/desktop-tools.css +11 -11
- package/src/renderer/src/css/sidebar-workspace.css +2 -2
- package/src/renderer/src/css/tool-surfaces.css +12 -12
- package/src/renderer/src/index.css +38 -20
- package/src/renderer/src/tauri.ts +22 -2
- package/src/renderer/src-web/App.tsx +14 -0
- package/src/renderer/src-web/agentProgress.ts +1 -0
- package/src/renderer/src-web/css/app-shell.css +4 -4
- package/src/renderer/src-web/css/chat.css +1278 -190
- package/src/renderer/src-web/css/mobile.css +29 -13
- package/src/renderer/src-web/css/settings/mobile.css +2 -2
- package/src/renderer/src-web/css/sidebar-workspace.css +1 -1
- package/src/renderer/src-web/index.css +38 -20
- package/src/renderer/src-web/tauri.ts +22 -2
- package/src/renderer/tests/browserArtifacts.browser.tsx +1 -1
- package/src/renderer/tests/library.browser.tsx +3 -0
- package/src/renderer/tests/liveVoice.browser.html +1 -0
- package/src/renderer/tests/workspaceDrop.browser.tsx +1 -1
- package/src-tauri/src/app/lifecycle.rs +1 -14
- package/src-tauri/src/background/headless.rs +14 -8
- package/src-tauri/src/commands/conversation.rs +38 -0
- package/src-tauri/src/native/mod.rs +0 -1
- package/src-tauri/src/native/system.rs +4 -47
- package/crates/mint-cli/src/interactive/chat_tui.rs +0 -8855
- package/crates/mint-core/src/agent/chat.rs +0 -4394
- package/crates/mint-core/src/browser/lifecycle.rs +0 -291
- package/crates/mint-core/src/browser/logging.rs +0 -20
- package/crates/mint-core/src/orchestration/tools/browser.rs +0 -31
- package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +0 -2
- package/out/web/assets/CalculationCard-CRjP3E1j.js +0 -1
- package/out/web/assets/CliSessionsView-C3Br2v5Y.css +0 -1
- package/out/web/assets/CodeReviewPage-CUJceQme.css +0 -1
- package/out/web/assets/CodeReviewPage-VCUgPla_.js +0 -2
- package/out/web/assets/ImageGenCard-BCXxkVJX.js +0 -1
- package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +0 -1
- package/out/web/assets/LibraryPage-DAxUTAg7.css +0 -1
- package/out/web/assets/LibraryPage-WlviRdR8.js +0 -2
- package/out/web/assets/LinkedFoldersView-CKRgroWK.js +0 -1
- package/out/web/assets/McpServersView-D3wHnuHg.js +0 -1
- package/out/web/assets/MermaidCard-NqrHdyJW.js +0 -2
- package/out/web/assets/MintDashboard-B2DJsDDO.css +0 -1
- package/out/web/assets/MintDashboard-COw_xol7.js +0 -63
- package/out/web/assets/RewindModal-DempuDod.js +0 -1
- package/out/web/assets/SettingsWindow-2YP3yj7s.js +0 -4
- package/out/web/assets/SettingsWindow-Dz8LQVgD.css +0 -1
- package/out/web/assets/StockCard-BDMCe7mx.js +0 -1
- package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +0 -38
- package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +0 -1
- package/out/web/assets/UiMockupWidget-DD73QWg1.js +0 -1
- package/out/web/assets/WeatherCard-CPucIfq8.js +0 -1
- package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +0 -2
- package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +0 -1
- package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +0 -1
- package/out/web/assets/config-CvIO5hkn.js +0 -2
- package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +0 -1
- package/out/web/assets/index-BmpulIkg.js +0 -168
- package/out/web/assets/index-CkvBwhX_.css +0 -1
- package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +0 -1
- package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +0 -8
- package/shared-modules/libsecret/libsecret.json +0 -23
- package/shared-modules/libusb/libusb.json +0 -27
- package/shared-modules/lzo/lzo.json +0 -27
- package/src/renderer/src/components/BrowserShell.tsx +0 -108
- package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +0 -91
- package/src/renderer/src/components/browser-shell.css +0 -185
- package/src-tauri/src/native/browser.rs +0 -252
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
use thiserror::Error;
|
|
4
|
+
|
|
5
|
+
use crate::MintConfig;
|
|
6
|
+
|
|
7
|
+
mod anthropic;
|
|
8
|
+
mod custom;
|
|
9
|
+
mod gemini;
|
|
10
|
+
mod ollama;
|
|
11
|
+
mod openai;
|
|
12
|
+
mod streaming;
|
|
13
|
+
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
mod tests;
|
|
16
|
+
|
|
17
|
+
use anthropic::*;
|
|
18
|
+
use custom::*;
|
|
19
|
+
pub(crate) use gemini::gemini_sanitize_schema;
|
|
20
|
+
use gemini::*;
|
|
21
|
+
use ollama::*;
|
|
22
|
+
use openai::*;
|
|
23
|
+
|
|
24
|
+
/// Provider-neutral pieces produced while a model response is still arriving.
|
|
25
|
+
/// Callers never need to know which vendor-specific field carried the data.
|
|
26
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
27
|
+
pub enum ChatStreamEvent {
|
|
28
|
+
TextDelta {
|
|
29
|
+
delta: String,
|
|
30
|
+
},
|
|
31
|
+
ReasoningDelta {
|
|
32
|
+
delta: String,
|
|
33
|
+
},
|
|
34
|
+
ToolCallDelta {
|
|
35
|
+
index: usize,
|
|
36
|
+
name: Option<String>,
|
|
37
|
+
arguments: Option<String>,
|
|
38
|
+
input: Option<Value>,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// `available_providers()`'s configured-providers list, reordered so
|
|
43
|
+
/// `gemini` is tried first among fallbacks (when it's configured and isn't
|
|
44
|
+
/// already the primary) — the user's explicit preference, ahead of
|
|
45
|
+
/// `available_providers()`'s own anthropic/openai/openrouter/deepseek/
|
|
46
|
+
/// gemini/... ordering (which exists for display purposes, e.g. the
|
|
47
|
+
/// `/models` picker, and is left alone). Everything else keeps its existing
|
|
48
|
+
/// relative order.
|
|
49
|
+
fn fallback_provider_order(config: &MintConfig) -> Vec<String> {
|
|
50
|
+
let mut providers = config.available_providers();
|
|
51
|
+
if let Some(pos) = providers.iter().position(|p| p == "gemini") {
|
|
52
|
+
let gemini = providers.remove(pos);
|
|
53
|
+
providers.insert(0, gemini);
|
|
54
|
+
}
|
|
55
|
+
providers
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Send a chat request, automatically falling back to other configured providers
|
|
59
|
+
/// if the primary one returns a recoverable error. Tries `gemini` first among
|
|
60
|
+
/// the fallbacks when it's configured (see `fallback_provider_order`), then
|
|
61
|
+
/// the rest in their usual order.
|
|
62
|
+
/// Returns `(response, Option<fallback_provider>)`.
|
|
63
|
+
pub async fn send_chat_with_fallback(
|
|
64
|
+
config: &MintConfig,
|
|
65
|
+
request: &ChatRequest,
|
|
66
|
+
) -> Result<(ChatResponse, Option<String>), ChatError> {
|
|
67
|
+
let primary_error = match send_chat(config, request).await {
|
|
68
|
+
Ok(r) => return Ok((r, None)),
|
|
69
|
+
Err(e) if !is_recoverable(&e) => return Err(e),
|
|
70
|
+
Err(e) => e,
|
|
71
|
+
};
|
|
72
|
+
// No point cycling through other cloud providers if the network itself is
|
|
73
|
+
// unreachable — they all sit behind the same dead link, so every attempt
|
|
74
|
+
// would just fail the same way after its own connect timeout. Surface
|
|
75
|
+
// this immediately so the caller can retry-and-wait instead.
|
|
76
|
+
if matches!(primary_error, ChatError::NetworkUnavailable) {
|
|
77
|
+
return Err(primary_error);
|
|
78
|
+
}
|
|
79
|
+
let fallback_reason = fallback_reason_text(&config.ai_provider, &primary_error);
|
|
80
|
+
for provider in fallback_provider_order(config) {
|
|
81
|
+
if provider.as_str() == config.ai_provider.as_str() {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
let alt = config_for_provider(config, &provider);
|
|
85
|
+
if let Ok(mut r) = send_chat(&alt, request).await {
|
|
86
|
+
r.fallback_provider = Some(config.ai_provider.clone());
|
|
87
|
+
r.fallback_reason = fallback_reason.clone();
|
|
88
|
+
return Ok((r, Some(provider)));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// all fallbacks failed — retry primary to surface original error
|
|
92
|
+
send_chat(config, request).await.map(|r| (r, None))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Short, human-readable explanation for why `send_chat_with_fallback` swapped
|
|
96
|
+
/// away from `provider` — `None` for an ordinary transient failure, so routine
|
|
97
|
+
/// hiccups don't get an extra notice while specific, actionable ones do.
|
|
98
|
+
fn fallback_reason_text(provider: &str, error: &ChatError) -> Option<String> {
|
|
99
|
+
let provider_name = if provider.starts_with("custom:") {
|
|
100
|
+
provider.strip_prefix("custom:").unwrap_or(provider)
|
|
101
|
+
} else {
|
|
102
|
+
provider
|
|
103
|
+
};
|
|
104
|
+
match error {
|
|
105
|
+
ChatError::InsufficientBalance(_) => Some(format!("{provider_name} ran out of balance")),
|
|
106
|
+
ChatError::Request(e) => {
|
|
107
|
+
if let Some(status) = e.status() {
|
|
108
|
+
if status == reqwest::StatusCode::PAYLOAD_TOO_LARGE {
|
|
109
|
+
Some(format!("{provider_name} payload too large (HTTP 413)"))
|
|
110
|
+
} else if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
|
|
111
|
+
Some(format!("{provider_name} rate limit exceeded (HTTP 429)"))
|
|
112
|
+
} else {
|
|
113
|
+
Some(format!("{provider_name} returned HTTP {status}"))
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
None
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
_ => None,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Stream a chat request with automatic provider fallback.
|
|
124
|
+
/// Returns `(response, Option<fallback_provider>)`.
|
|
125
|
+
pub async fn stream_chat_with_fallback<F>(
|
|
126
|
+
config: &MintConfig,
|
|
127
|
+
request: &ChatRequest,
|
|
128
|
+
mut on_chunk: F,
|
|
129
|
+
) -> Result<(ChatResponse, Option<String>), ChatError>
|
|
130
|
+
where
|
|
131
|
+
F: FnMut(String),
|
|
132
|
+
{
|
|
133
|
+
let primary_error = match stream_chat(config, request, &mut on_chunk).await {
|
|
134
|
+
Ok(r) => return Ok((r, None)),
|
|
135
|
+
Err(e) if !is_recoverable(&e) => return Err(e),
|
|
136
|
+
Err(e) => {
|
|
137
|
+
eprintln!("Ollama stream chat failed, falling back: {:?}", e);
|
|
138
|
+
e
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
if matches!(primary_error, ChatError::NetworkUnavailable) {
|
|
142
|
+
return Err(primary_error);
|
|
143
|
+
}
|
|
144
|
+
let fallback_reason = fallback_reason_text(&config.ai_provider, &primary_error);
|
|
145
|
+
for provider in fallback_provider_order(config) {
|
|
146
|
+
if provider.as_str() == config.ai_provider.as_str() {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let alt = config_for_provider(config, &provider);
|
|
150
|
+
if let Ok(mut r) = stream_chat(&alt, request, &mut on_chunk).await {
|
|
151
|
+
r.fallback_provider = Some(config.ai_provider.clone());
|
|
152
|
+
r.fallback_reason = fallback_reason.clone();
|
|
153
|
+
return Ok((r, Some(provider)));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
stream_chat(config, request, &mut on_chunk)
|
|
157
|
+
.await
|
|
158
|
+
.map(|r| (r, None))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Streaming equivalent of [`send_chat_with_fallback`] that preserves the
|
|
162
|
+
/// complete response (tool calls, usage, stop reason, and reasoning) while
|
|
163
|
+
/// exposing provider-neutral deltas as they arrive.
|
|
164
|
+
pub async fn stream_chat_events_with_fallback<F>(
|
|
165
|
+
config: &MintConfig,
|
|
166
|
+
request: &ChatRequest,
|
|
167
|
+
mut on_event: F,
|
|
168
|
+
) -> Result<(ChatResponse, Option<String>), ChatError>
|
|
169
|
+
where
|
|
170
|
+
F: FnMut(ChatStreamEvent),
|
|
171
|
+
{
|
|
172
|
+
let primary_error = match stream_chat_events(config, request, &mut on_event).await {
|
|
173
|
+
Ok(r) => return Ok((r, None)),
|
|
174
|
+
Err(e) if !is_recoverable(&e) => return Err(e),
|
|
175
|
+
Err(e) => e,
|
|
176
|
+
};
|
|
177
|
+
if matches!(primary_error, ChatError::NetworkUnavailable) {
|
|
178
|
+
return Err(primary_error);
|
|
179
|
+
}
|
|
180
|
+
let fallback_reason = fallback_reason_text(&config.ai_provider, &primary_error);
|
|
181
|
+
for provider in fallback_provider_order(config) {
|
|
182
|
+
if provider == config.ai_provider {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
let alt = config_for_provider(config, &provider);
|
|
186
|
+
if let Ok(mut response) = stream_chat_events(&alt, request, &mut on_event).await {
|
|
187
|
+
response.fallback_provider = Some(config.ai_provider.clone());
|
|
188
|
+
response.fallback_reason = fallback_reason.clone();
|
|
189
|
+
return Ok((response, Some(provider)));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
stream_chat_events(config, request, &mut on_event)
|
|
193
|
+
.await
|
|
194
|
+
.map(|response| (response, None))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Whether an error warrants trying another provider.
|
|
198
|
+
fn is_recoverable(e: &ChatError) -> bool {
|
|
199
|
+
matches!(
|
|
200
|
+
e,
|
|
201
|
+
ChatError::MissingApiKey(_)
|
|
202
|
+
| ChatError::Request(_)
|
|
203
|
+
| ChatError::MissingResponseText
|
|
204
|
+
| ChatError::UnsupportedAttachments(_)
|
|
205
|
+
| ChatError::InsufficientBalance(_)
|
|
206
|
+
| ChatError::NetworkUnavailable
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Clone the config with a different active provider.
|
|
211
|
+
fn config_for_provider(config: &MintConfig, provider: &str) -> MintConfig {
|
|
212
|
+
MintConfig {
|
|
213
|
+
ai_provider: provider.to_owned(),
|
|
214
|
+
..config.clone()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[derive(Debug, Clone, Deserialize, Default)]
|
|
219
|
+
#[serde(rename_all = "camelCase")]
|
|
220
|
+
pub struct ChatRequest {
|
|
221
|
+
pub message: String,
|
|
222
|
+
#[serde(default)]
|
|
223
|
+
pub system_instruction: String,
|
|
224
|
+
#[serde(default)]
|
|
225
|
+
pub chat_id: Option<String>,
|
|
226
|
+
#[serde(default)]
|
|
227
|
+
pub image_data_uri: Option<String>,
|
|
228
|
+
#[serde(default)]
|
|
229
|
+
pub audio_data_uri: Option<String>,
|
|
230
|
+
#[serde(default)]
|
|
231
|
+
pub video_data_uri: Option<String>,
|
|
232
|
+
#[serde(default)]
|
|
233
|
+
pub document_attachment: Option<DocumentAttachment>,
|
|
234
|
+
#[serde(default)]
|
|
235
|
+
pub workspace_path: Option<String>,
|
|
236
|
+
#[serde(default)]
|
|
237
|
+
pub agent_id: Option<String>,
|
|
238
|
+
#[serde(default)]
|
|
239
|
+
pub plan_mode: bool,
|
|
240
|
+
/// When set, restricts this turn's `mcp_tool`/`mcp_list_tools` calls to this one
|
|
241
|
+
/// configured MCP server — set by picking a plugin from the composer's `@` mention
|
|
242
|
+
/// dropdown, rather than leaving server selection to the model's own judgment.
|
|
243
|
+
#[serde(default)]
|
|
244
|
+
pub pinned_mcp_server: Option<String>,
|
|
245
|
+
/// Structured conversation history for native tool-calling. When set, provider
|
|
246
|
+
/// adapters build the request from this instead of the flat `message` field.
|
|
247
|
+
#[serde(default)]
|
|
248
|
+
pub messages: Option<Vec<ChatMessage>>,
|
|
249
|
+
/// Tool definitions to offer the model via each provider's native
|
|
250
|
+
/// function/tool-calling API. Ignored when `messages` is `None`.
|
|
251
|
+
#[serde(default)]
|
|
252
|
+
pub tools: Option<Vec<ToolSpec>>,
|
|
253
|
+
/// Optional per-request temperature override. When `None`, falls back to
|
|
254
|
+
/// `config.resolved_temperature()`.
|
|
255
|
+
#[serde(default)]
|
|
256
|
+
pub temperature: Option<f64>,
|
|
257
|
+
/// Optional per-request thinking/reasoning enabled override.
|
|
258
|
+
#[serde(default)]
|
|
259
|
+
pub thinking_enabled: Option<bool>,
|
|
260
|
+
/// Optional per-request thinking effort override ("low", "medium", "high", "extra_high").
|
|
261
|
+
#[serde(default)]
|
|
262
|
+
pub thinking_effort: Option<String>,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
266
|
+
#[serde(rename_all = "camelCase")]
|
|
267
|
+
pub enum ChatRole {
|
|
268
|
+
System,
|
|
269
|
+
User,
|
|
270
|
+
Assistant,
|
|
271
|
+
Tool,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
275
|
+
#[serde(rename_all = "camelCase")]
|
|
276
|
+
pub struct ChatMessage {
|
|
277
|
+
pub role: ChatRole,
|
|
278
|
+
pub content: Vec<ContentBlock>,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
impl ChatMessage {
|
|
282
|
+
pub fn text(role: ChatRole, text: impl Into<String>) -> Self {
|
|
283
|
+
ChatMessage {
|
|
284
|
+
role,
|
|
285
|
+
content: vec![ContentBlock::Text { text: text.into() }],
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
291
|
+
#[serde(tag = "type", rename_all = "camelCase")]
|
|
292
|
+
pub enum ContentBlock {
|
|
293
|
+
Text {
|
|
294
|
+
text: String,
|
|
295
|
+
},
|
|
296
|
+
ToolUse {
|
|
297
|
+
id: String,
|
|
298
|
+
name: String,
|
|
299
|
+
input: Value,
|
|
300
|
+
/// See `ToolCall::thought_signature` — carried on `ToolUse` too so it
|
|
301
|
+
/// survives being replayed back into `native_messages` history on the
|
|
302
|
+
/// next turn. `None` for every provider but Gemini.
|
|
303
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
304
|
+
thought_signature: Option<String>,
|
|
305
|
+
},
|
|
306
|
+
ToolResult {
|
|
307
|
+
tool_use_id: String,
|
|
308
|
+
content: String,
|
|
309
|
+
#[serde(default)]
|
|
310
|
+
is_error: bool,
|
|
311
|
+
},
|
|
312
|
+
Image {
|
|
313
|
+
data_uri: String,
|
|
314
|
+
},
|
|
315
|
+
Audio {
|
|
316
|
+
data_uri: String,
|
|
317
|
+
},
|
|
318
|
+
Video {
|
|
319
|
+
data_uri: String,
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// A tool definition offered to the model via native function/tool-calling.
|
|
324
|
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
325
|
+
#[serde(rename_all = "camelCase")]
|
|
326
|
+
pub struct ToolSpec {
|
|
327
|
+
pub name: String,
|
|
328
|
+
pub description: String,
|
|
329
|
+
/// JSON schema (draft-7-ish, as accepted by the provider's tool-parameter format).
|
|
330
|
+
pub parameters: Value,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// A tool invocation the model requested via native function/tool-calling.
|
|
334
|
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
335
|
+
#[serde(rename_all = "camelCase")]
|
|
336
|
+
pub struct ToolCall {
|
|
337
|
+
pub id: String,
|
|
338
|
+
pub name: String,
|
|
339
|
+
pub input: Value,
|
|
340
|
+
/// Gemini 3.x's "thinking" models attach an opaque `thoughtSignature` to
|
|
341
|
+
/// each `functionCall` part; it must be echoed back verbatim on the next
|
|
342
|
+
/// turn's replayed `functionCall` or the API rejects the request with a
|
|
343
|
+
/// 400 (`INVALID_ARGUMENT: Function call is missing a thought_signature`).
|
|
344
|
+
/// `None` for every other provider.
|
|
345
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
346
|
+
pub thought_signature: Option<String>,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
350
|
+
#[serde(rename_all = "camelCase")]
|
|
351
|
+
pub struct DocumentAttachment {
|
|
352
|
+
pub filename: String,
|
|
353
|
+
pub data_uri: String,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const MAX_DOCUMENT_BYTES: usize = 10 * 1024 * 1024;
|
|
357
|
+
const MAX_DOCUMENT_CONTEXT_CHARS: usize = 400_000;
|
|
358
|
+
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
|
359
|
+
|
|
360
|
+
impl ChatRequest {
|
|
361
|
+
pub(crate) fn with_stored_document_context(
|
|
362
|
+
&self,
|
|
363
|
+
_config: &crate::MintConfig,
|
|
364
|
+
path: &std::path::Path,
|
|
365
|
+
) -> Result<ChatRequest, String> {
|
|
366
|
+
let Some(document) = &self.document_attachment else {
|
|
367
|
+
return Ok(self.clone());
|
|
368
|
+
};
|
|
369
|
+
let extracted =
|
|
370
|
+
crate::knowledge::extract_managed_document_text(path).map_err(|e| e.to_string())?;
|
|
371
|
+
self.with_document_text(document, extracted)
|
|
372
|
+
}
|
|
373
|
+
fn with_document_text(
|
|
374
|
+
&self,
|
|
375
|
+
document: &DocumentAttachment,
|
|
376
|
+
extracted: String,
|
|
377
|
+
) -> Result<ChatRequest, String> {
|
|
378
|
+
let mut context: String = extracted.chars().take(MAX_DOCUMENT_CONTEXT_CHARS).collect();
|
|
379
|
+
if extracted.chars().count() > MAX_DOCUMENT_CONTEXT_CHARS {
|
|
380
|
+
context.push_str("\n\n[Document text truncated because it is long.]");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let filename = document.filename.trim();
|
|
384
|
+
let filename = if filename.is_empty() {
|
|
385
|
+
"attached document"
|
|
386
|
+
} else {
|
|
387
|
+
filename
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
let mut enriched = self.clone();
|
|
391
|
+
enriched.document_attachment = None;
|
|
392
|
+
enriched.message = format!(
|
|
393
|
+
"{}\n\nAttached document: {filename}\nUse the extracted document text below when answering. If the user asks for a summary, summarize this document.\n\n--- Extracted document text ---\n{context}\n--- End extracted document text ---",
|
|
394
|
+
self.message
|
|
395
|
+
);
|
|
396
|
+
Ok(enriched)
|
|
397
|
+
}
|
|
398
|
+
pub fn with_document_context(&self, config: &crate::MintConfig) -> Result<ChatRequest, String> {
|
|
399
|
+
let Some(document) = &self.document_attachment else {
|
|
400
|
+
return Ok(self.clone());
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
let (_mime_type, encoded) = document
|
|
404
|
+
.data_uri
|
|
405
|
+
.strip_prefix("data:")
|
|
406
|
+
.and_then(|payload| payload.split_once(";base64,"))
|
|
407
|
+
.ok_or_else(|| "invalid document attachment data URI".to_string())?;
|
|
408
|
+
|
|
409
|
+
// Dispatch by the filename's extension, not the data URI's MIME type:
|
|
410
|
+
// browsers report an empty or generic `application/octet-stream` type
|
|
411
|
+
// for many of these formats (.md, .toml, .yaml, ...), so MIME type
|
|
412
|
+
// isn't a reliable signal here. `extract_document_text` below performs
|
|
413
|
+
// the real allowlist check and rejects anything it doesn't recognize.
|
|
414
|
+
let extension = std::path::Path::new(document.filename.trim())
|
|
415
|
+
.extension()
|
|
416
|
+
.and_then(|ext| ext.to_str())
|
|
417
|
+
.map(|ext| ext.to_ascii_lowercase())
|
|
418
|
+
.filter(|ext| !ext.is_empty())
|
|
419
|
+
.unwrap_or_else(|| "txt".to_string());
|
|
420
|
+
|
|
421
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
|
422
|
+
let bytes = BASE64_STANDARD
|
|
423
|
+
.decode(encoded)
|
|
424
|
+
.map_err(|error| format!("invalid document attachment encoding: {error}"))?;
|
|
425
|
+
if bytes.len() > MAX_DOCUMENT_BYTES {
|
|
426
|
+
return Err("document attachment is too large (> 10 MiB)".into());
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let directory = crate::config::config_path()
|
|
430
|
+
.map_err(|error| error.to_string())?
|
|
431
|
+
.with_file_name("Attachments");
|
|
432
|
+
std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
|
|
433
|
+
|
|
434
|
+
let safe_name: String = document
|
|
435
|
+
.filename
|
|
436
|
+
.chars()
|
|
437
|
+
.filter(|c| c.is_ascii_alphanumeric() || matches!(*c, '.' | '-' | '_'))
|
|
438
|
+
.take(80)
|
|
439
|
+
.collect();
|
|
440
|
+
let safe_name = if safe_name.is_empty() {
|
|
441
|
+
"document".into()
|
|
442
|
+
} else {
|
|
443
|
+
safe_name
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
let path = directory.join(format!(
|
|
447
|
+
"mint-doc-{}-{}.{}",
|
|
448
|
+
COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
|
|
449
|
+
safe_name,
|
|
450
|
+
extension
|
|
451
|
+
));
|
|
452
|
+
std::fs::write(&path, bytes).map_err(|error| error.to_string())?;
|
|
453
|
+
|
|
454
|
+
let extracted = crate::knowledge::extract_document_text(&path, config)
|
|
455
|
+
.map_err(|error| error.to_string());
|
|
456
|
+
let _ = std::fs::remove_file(&path);
|
|
457
|
+
let extracted = extracted?;
|
|
458
|
+
|
|
459
|
+
self.with_document_text(document, extracted)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#[derive(Debug, Clone, Serialize)]
|
|
464
|
+
#[serde(rename_all = "camelCase")]
|
|
465
|
+
pub struct ChatResponse {
|
|
466
|
+
pub provider: String,
|
|
467
|
+
pub model: String,
|
|
468
|
+
pub text: String,
|
|
469
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
470
|
+
pub fallback_provider: Option<String>,
|
|
471
|
+
/// Short, human-readable reason `send_chat_with_fallback` swapped providers,
|
|
472
|
+
/// when the primary's failure was specific enough to explain (insufficient
|
|
473
|
+
/// balance, no network) — `None` for an ordinary transient error, so a
|
|
474
|
+
/// ordinary hiccup doesn't get an extra notice while a genuinely actionable
|
|
475
|
+
/// one does.
|
|
476
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
477
|
+
pub fallback_reason: Option<String>,
|
|
478
|
+
/// Tool calls the model requested via native function/tool-calling, when the
|
|
479
|
+
/// request set `ChatRequest.tools` and the provider honored it.
|
|
480
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
481
|
+
pub tool_calls: Option<Vec<ToolCall>>,
|
|
482
|
+
/// Provider-reported reason generation stopped (e.g. `"tool_use"`, `"end_turn"`,
|
|
483
|
+
/// `"stop"`, `"max_tokens"`), when the provider reports one.
|
|
484
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
485
|
+
pub stop_reason: Option<String>,
|
|
486
|
+
/// Total tokens (input + output) the provider billed for this turn, when
|
|
487
|
+
/// reported. Used to track how close a long-running conversation is to the
|
|
488
|
+
/// model's context window, rather than estimating client-side. Derived from
|
|
489
|
+
/// `input_tokens + output_tokens` when both are present.
|
|
490
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
491
|
+
pub total_tokens: Option<u32>,
|
|
492
|
+
/// Prompt/context tokens the provider processed for this turn (the whole
|
|
493
|
+
/// resent history + system prompt + tool schemas), when reported — the
|
|
494
|
+
/// "sent up to the model" half, tracked separately from `output_tokens` so
|
|
495
|
+
/// the UI can show `↑ context · ↓ generated` instead of one opaque sum.
|
|
496
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
497
|
+
pub input_tokens: Option<u32>,
|
|
498
|
+
/// Completion tokens the model generated this turn, when reported — the
|
|
499
|
+
/// "came back down from the model" half. See `input_tokens`.
|
|
500
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
501
|
+
pub output_tokens: Option<u32>,
|
|
502
|
+
/// Model reasoning / chain-of-thought tokens, when reported by reasoning models
|
|
503
|
+
/// (e.g. DeepSeek Reasoner, Claude 3.7 Thinking, Gemini Thinking, or <think> tags).
|
|
504
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
505
|
+
pub thought: Option<String>,
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/// Internal, provider-agnostic result of a single non-streaming provider call.
|
|
509
|
+
/// `send_chat` maps this onto the public `ChatResponse`.
|
|
510
|
+
pub(super) struct ProviderReply {
|
|
511
|
+
pub(super) model: String,
|
|
512
|
+
pub(super) text: String,
|
|
513
|
+
pub(super) thought: Option<String>,
|
|
514
|
+
pub(super) tool_calls: Option<Vec<ToolCall>>,
|
|
515
|
+
pub(super) stop_reason: Option<String>,
|
|
516
|
+
/// Prompt/context tokens processed for this call, when the provider reports
|
|
517
|
+
/// usage. `send_chat` derives `ChatResponse::total_tokens` from this plus
|
|
518
|
+
/// `output_tokens`.
|
|
519
|
+
pub(super) input_tokens: Option<u32>,
|
|
520
|
+
/// Completion tokens generated for this call, when reported.
|
|
521
|
+
pub(super) output_tokens: Option<u32>,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[derive(Debug, Error)]
|
|
525
|
+
pub enum ChatError {
|
|
526
|
+
#[error("provider '{0}' is not implemented in the Rust backend yet")]
|
|
527
|
+
UnsupportedProvider(String),
|
|
528
|
+
#[error("missing API key for provider '{0}'")]
|
|
529
|
+
MissingApiKey(String),
|
|
530
|
+
#[error("provider request failed: {0}")]
|
|
531
|
+
Request(#[from] reqwest::Error),
|
|
532
|
+
#[error("provider response did not include assistant text")]
|
|
533
|
+
MissingResponseText,
|
|
534
|
+
#[error("provider '{0}' does not support Mint multimodal attachments yet")]
|
|
535
|
+
UnsupportedAttachments(String),
|
|
536
|
+
#[error("invalid multimodal data URI")]
|
|
537
|
+
InvalidAttachment,
|
|
538
|
+
/// Provider returned `402 Payment Required` — distinguished from the generic
|
|
539
|
+
/// `Request` case so callers can tell "your account ran out of balance" apart
|
|
540
|
+
/// from an ordinary transient failure instead of silently swapping providers
|
|
541
|
+
/// with no explanation.
|
|
542
|
+
#[error("provider '{0}' rejected the request: insufficient balance")]
|
|
543
|
+
InsufficientBalance(String),
|
|
544
|
+
/// The request never reached any server at all (DNS failure, connection
|
|
545
|
+
/// refused, no route) — as opposed to reaching a server that returned an
|
|
546
|
+
/// error. Distinguished so a caller can retry-and-wait instead of pointlessly
|
|
547
|
+
/// cycling through other cloud providers that sit behind the same dead
|
|
548
|
+
/// network link.
|
|
549
|
+
#[error("no network connection to reach the provider")]
|
|
550
|
+
NetworkUnavailable,
|
|
551
|
+
#[error("provider API error: {0}")]
|
|
552
|
+
Api(String),
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
pub async fn send_chat(
|
|
556
|
+
config: &MintConfig,
|
|
557
|
+
request: &ChatRequest,
|
|
558
|
+
) -> Result<ChatResponse, ChatError> {
|
|
559
|
+
let client = crate::HTTP_CLIENT.clone();
|
|
560
|
+
let provider = config.ai_provider.as_str();
|
|
561
|
+
require_supported_attachments(provider, request)?;
|
|
562
|
+
let reply = match provider {
|
|
563
|
+
"gemini" => call_gemini(&client, config, request).await?,
|
|
564
|
+
"openai" | "local_openai" | "openrouter" | "deepseek" => {
|
|
565
|
+
call_openai(&client, config, request).await?
|
|
566
|
+
}
|
|
567
|
+
"ollama" => call_ollama(&client, config, request).await?,
|
|
568
|
+
"anthropic" => call_anthropic(&client, config, request).await?,
|
|
569
|
+
"huggingface" => call_huggingface(&client, config, request).await?,
|
|
570
|
+
p if p.starts_with("custom:") => call_custom_provider(&client, config, request).await?,
|
|
571
|
+
other => return Err(ChatError::UnsupportedProvider(other.into())),
|
|
572
|
+
};
|
|
573
|
+
Ok(ChatResponse {
|
|
574
|
+
provider: provider.into(),
|
|
575
|
+
model: reply.model,
|
|
576
|
+
text: reply.text,
|
|
577
|
+
thought: reply.thought,
|
|
578
|
+
fallback_provider: None,
|
|
579
|
+
fallback_reason: None,
|
|
580
|
+
tool_calls: reply.tool_calls,
|
|
581
|
+
stop_reason: reply.stop_reason,
|
|
582
|
+
total_tokens: match (reply.input_tokens, reply.output_tokens) {
|
|
583
|
+
(Some(i), Some(o)) => Some(i + o),
|
|
584
|
+
(i, o) => i.or(o),
|
|
585
|
+
},
|
|
586
|
+
input_tokens: reply.input_tokens,
|
|
587
|
+
output_tokens: reply.output_tokens,
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
pub async fn stream_chat<F>(
|
|
592
|
+
config: &MintConfig,
|
|
593
|
+
request: &ChatRequest,
|
|
594
|
+
mut on_chunk: F,
|
|
595
|
+
) -> Result<ChatResponse, ChatError>
|
|
596
|
+
where
|
|
597
|
+
F: FnMut(String),
|
|
598
|
+
{
|
|
599
|
+
stream_chat_events(config, request, |event| {
|
|
600
|
+
if let ChatStreamEvent::TextDelta { delta } = event {
|
|
601
|
+
on_chunk(delta);
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
.await
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
pub async fn stream_chat_events<F>(
|
|
608
|
+
config: &MintConfig,
|
|
609
|
+
request: &ChatRequest,
|
|
610
|
+
mut on_event: F,
|
|
611
|
+
) -> Result<ChatResponse, ChatError>
|
|
612
|
+
where
|
|
613
|
+
F: FnMut(ChatStreamEvent),
|
|
614
|
+
{
|
|
615
|
+
let client = crate::HTTP_CLIENT.clone();
|
|
616
|
+
let provider = config.ai_provider.as_str();
|
|
617
|
+
require_supported_attachments(provider, request)?;
|
|
618
|
+
let reply = match provider {
|
|
619
|
+
"gemini" => stream_gemini_reply(&client, config, request, &mut on_event).await?,
|
|
620
|
+
"openai" | "local_openai" | "openrouter" | "deepseek" => {
|
|
621
|
+
stream_openai_reply(&client, config, request, &mut on_event).await?
|
|
622
|
+
}
|
|
623
|
+
"ollama" => stream_ollama_reply(&client, config, request, &mut on_event).await?,
|
|
624
|
+
"anthropic" => stream_anthropic_reply(&client, config, request, &mut on_event).await?,
|
|
625
|
+
"huggingface" => stream_huggingface_reply(&client, config, request, &mut on_event).await?,
|
|
626
|
+
p if p.starts_with("custom:") => {
|
|
627
|
+
stream_custom_provider_reply(&client, config, request, &mut on_event).await?
|
|
628
|
+
}
|
|
629
|
+
other => return Err(ChatError::UnsupportedProvider(other.into())),
|
|
630
|
+
};
|
|
631
|
+
Ok(ChatResponse {
|
|
632
|
+
provider: provider.into(),
|
|
633
|
+
model: reply.model,
|
|
634
|
+
text: reply.text,
|
|
635
|
+
thought: reply.thought,
|
|
636
|
+
fallback_provider: None,
|
|
637
|
+
fallback_reason: None,
|
|
638
|
+
tool_calls: reply.tool_calls,
|
|
639
|
+
stop_reason: reply.stop_reason,
|
|
640
|
+
total_tokens: match (reply.input_tokens, reply.output_tokens) {
|
|
641
|
+
(Some(input), Some(output)) => Some(input + output),
|
|
642
|
+
(input, output) => input.or(output),
|
|
643
|
+
},
|
|
644
|
+
input_tokens: reply.input_tokens,
|
|
645
|
+
output_tokens: reply.output_tokens,
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/// Extracts `<think>...</think>` or `<thought>...</thought>` chain-of-thought blocks emitted by
|
|
650
|
+
/// reasoning models or the prompt-based CoT fallback when embedded directly into plain text content.
|
|
651
|
+
/// Returns `(Option<thought>, remaining_text)`.
|
|
652
|
+
pub(crate) fn extract_think_tag(text: &str) -> (Option<String>, String) {
|
|
653
|
+
for (open_tag, close_tag) in [("<think>", "</think>"), ("<thought>", "</thought>")] {
|
|
654
|
+
if let Some(start) = text.find(open_tag) {
|
|
655
|
+
let open_len = open_tag.len();
|
|
656
|
+
let close_len = close_tag.len();
|
|
657
|
+
if let Some(end) = text[start + open_len..].find(close_tag) {
|
|
658
|
+
let thought = text[start + open_len..start + open_len + end]
|
|
659
|
+
.trim()
|
|
660
|
+
.to_string();
|
|
661
|
+
let mut remaining = text[..start].to_string();
|
|
662
|
+
remaining.push_str(&text[start + open_len + end + close_len..]);
|
|
663
|
+
let cleaned = remaining.trim().to_string();
|
|
664
|
+
return (
|
|
665
|
+
if thought.is_empty() {
|
|
666
|
+
None
|
|
667
|
+
} else {
|
|
668
|
+
Some(thought)
|
|
669
|
+
},
|
|
670
|
+
cleaned,
|
|
671
|
+
);
|
|
672
|
+
} else {
|
|
673
|
+
let thought = text[start + open_len..].trim().to_string();
|
|
674
|
+
let cleaned = text[..start].trim().to_string();
|
|
675
|
+
return (
|
|
676
|
+
if thought.is_empty() {
|
|
677
|
+
None
|
|
678
|
+
} else {
|
|
679
|
+
Some(thought)
|
|
680
|
+
},
|
|
681
|
+
cleaned,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
(None, text.to_string())
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
pub(super) fn required_key(provider: &str, key: &str) -> Result<(), ChatError> {
|
|
690
|
+
if key.trim().is_empty() {
|
|
691
|
+
Err(ChatError::MissingApiKey(provider.into()))
|
|
692
|
+
} else {
|
|
693
|
+
Ok(())
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
pub(super) fn provider_key(configured: &str, environment_variable: &str) -> String {
|
|
698
|
+
if configured.trim().is_empty() {
|
|
699
|
+
std::env::var(environment_variable).unwrap_or_default()
|
|
700
|
+
} else {
|
|
701
|
+
configured.into()
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
pub(super) fn require_supported_attachments(
|
|
706
|
+
provider: &str,
|
|
707
|
+
request: &ChatRequest,
|
|
708
|
+
) -> Result<(), ChatError> {
|
|
709
|
+
let has_image = request
|
|
710
|
+
.image_data_uri
|
|
711
|
+
.as_ref()
|
|
712
|
+
.is_some_and(|s| !s.trim().is_empty());
|
|
713
|
+
let has_audio = request
|
|
714
|
+
.audio_data_uri
|
|
715
|
+
.as_ref()
|
|
716
|
+
.is_some_and(|s| !s.trim().is_empty());
|
|
717
|
+
let has_video = request
|
|
718
|
+
.video_data_uri
|
|
719
|
+
.as_ref()
|
|
720
|
+
.is_some_and(|s| !s.trim().is_empty());
|
|
721
|
+
|
|
722
|
+
match provider {
|
|
723
|
+
"gemini" => Ok(()),
|
|
724
|
+
"ollama" if has_audio || has_video => {
|
|
725
|
+
Err(ChatError::UnsupportedAttachments(provider.into()))
|
|
726
|
+
}
|
|
727
|
+
"ollama" => Ok(()),
|
|
728
|
+
// The real OpenAI API supports audio input natively (`input_audio` content
|
|
729
|
+
// parts, see `openai_audio_part`) — video still isn't implemented for it.
|
|
730
|
+
"openai" if has_video => Err(ChatError::UnsupportedAttachments(provider.into())),
|
|
731
|
+
"openai" => Ok(()),
|
|
732
|
+
// These all build their payload via `openai_chat_payload`/`anthropic_chat_payload`,
|
|
733
|
+
// which support image attachments (see `openai_message_content`/
|
|
734
|
+
// `anthropic_message_content`). Unlike literal `"openai"` above, these are
|
|
735
|
+
// separate services/proxies that aren't confirmed to speak the same
|
|
736
|
+
// `input_audio` schema, so audio/video stay unsupported for them.
|
|
737
|
+
"local_openai" | "openrouter" | "deepseek" | "huggingface" | "anthropic"
|
|
738
|
+
if has_audio || has_video =>
|
|
739
|
+
{
|
|
740
|
+
Err(ChatError::UnsupportedAttachments(provider.into()))
|
|
741
|
+
}
|
|
742
|
+
"local_openai" | "openrouter" | "deepseek" | "huggingface" | "anthropic" => Ok(()),
|
|
743
|
+
// Custom providers route through `call_custom_provider`, which also uses
|
|
744
|
+
// `openai_chat_payload`, so images are supported the same way; audio/video
|
|
745
|
+
// are not implemented for them yet.
|
|
746
|
+
p if p.starts_with("custom:") && (has_audio || has_video) => {
|
|
747
|
+
Err(ChatError::UnsupportedAttachments(provider.into()))
|
|
748
|
+
}
|
|
749
|
+
p if p.starts_with("custom:") => Ok(()),
|
|
750
|
+
_ if has_image || has_audio || has_video => {
|
|
751
|
+
Err(ChatError::UnsupportedAttachments(provider.into()))
|
|
752
|
+
}
|
|
753
|
+
_ => Ok(()),
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
pub(super) fn wants_agent_json(request: &ChatRequest) -> bool {
|
|
758
|
+
let instruction = request.system_instruction.as_str();
|
|
759
|
+
instruction.contains("Return only JSON")
|
|
760
|
+
|| (instruction.contains("Return exactly one JSON object per response")
|
|
761
|
+
&& instruction.contains("Input formats:")
|
|
762
|
+
&& instruction.contains("- finish:"))
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/// Resolves the effective sampling temperature for this request, prioritizing
|
|
766
|
+
/// `request.temperature` if present, otherwise falling back to `config.resolved_temperature()`.
|
|
767
|
+
pub(crate) fn effective_temperature(config: &MintConfig, request: &ChatRequest) -> f64 {
|
|
768
|
+
request
|
|
769
|
+
.temperature
|
|
770
|
+
.unwrap_or_else(|| config.resolved_temperature())
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/// Resolves (enabled, effort) for this request and model, prioritizing request overrides.
|
|
774
|
+
pub(crate) fn effective_thinking(
|
|
775
|
+
config: &MintConfig,
|
|
776
|
+
request: &ChatRequest,
|
|
777
|
+
model: &str,
|
|
778
|
+
) -> (bool, String) {
|
|
779
|
+
if !MintConfig::is_thinking_supported_for_model(&config.ai_provider, model) {
|
|
780
|
+
return (false, "medium".to_string());
|
|
781
|
+
}
|
|
782
|
+
let enabled = request
|
|
783
|
+
.thinking_enabled
|
|
784
|
+
.unwrap_or_else(|| config.resolved_thinking_enabled_for_model(&config.ai_provider, model));
|
|
785
|
+
let effort = request
|
|
786
|
+
.thinking_effort
|
|
787
|
+
.as_deref()
|
|
788
|
+
.map(str::to_ascii_lowercase)
|
|
789
|
+
.unwrap_or_else(|| config.resolved_thinking_effort_for_model(model).to_string());
|
|
790
|
+
(enabled, effort)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
pub(crate) fn thinking_prompt_directive(effort: &str) -> &'static str {
|
|
794
|
+
match effort {
|
|
795
|
+
"low" => {
|
|
796
|
+
"\n\n[Thinking Mode: Enabled (Low Effort)]\nBefore providing your response or calling any tool, you MUST write a brief and concise (2-4 sentences) internal reasoning process enclosed strictly within <think>...</think> tags at the very beginning of your message content (never inside tool arguments), followed by your tool call or final answer outside the tags."
|
|
797
|
+
}
|
|
798
|
+
"high" => {
|
|
799
|
+
"\n\n[Thinking Mode: Enabled (High Effort)]\nBefore providing your response or calling any tool, you MUST write a thorough and detailed internal reasoning process, carefully analyzing edge cases and trade-offs, enclosed strictly within <think>...</think> tags at the very beginning of your message content (never inside tool arguments), followed by your tool call or final answer outside the tags."
|
|
800
|
+
}
|
|
801
|
+
"extra_high" => {
|
|
802
|
+
"\n\n[Thinking Mode: Enabled (Extra High Effort)]\nBefore providing your response or calling any tool, you MUST write an exhaustive, step-by-step internal reasoning process, verifying assumptions and alternative approaches in depth, enclosed strictly within <think>...</think> tags at the very beginning of your message content (never inside tool arguments), followed by your tool call or final answer outside the tags."
|
|
803
|
+
}
|
|
804
|
+
_ => {
|
|
805
|
+
"\n\n[Thinking Mode: Enabled (Medium Effort)]\nBefore providing your response or calling any tool, you MUST write a clear and structured internal reasoning process enclosed strictly within <think>...</think> tags at the very beginning of your message content (never inside tool arguments), followed by your tool call or final answer outside the tags."
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/// Returns the effective system instruction for a request, appending a `<think>...</think>`
|
|
811
|
+
/// Chain-of-Thought directive when Thinking is enabled on a chat model that does not use
|
|
812
|
+
/// a native visible reasoning API parameter.
|
|
813
|
+
pub(crate) fn effective_system_instruction(
|
|
814
|
+
config: &MintConfig,
|
|
815
|
+
request: &ChatRequest,
|
|
816
|
+
provider: &str,
|
|
817
|
+
model: &str,
|
|
818
|
+
) -> String {
|
|
819
|
+
let base = request.system_instruction.as_str();
|
|
820
|
+
if wants_agent_json(request) {
|
|
821
|
+
return base.to_string();
|
|
822
|
+
}
|
|
823
|
+
let (enabled, effort) = effective_thinking(config, request, model);
|
|
824
|
+
if enabled && !MintConfig::has_native_thinking_api(provider, model) {
|
|
825
|
+
format!("{base}{}", thinking_prompt_directive(&effort))
|
|
826
|
+
} else {
|
|
827
|
+
base.to_string()
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/// Concatenates all `Text` blocks in a message (tool-use/result/image blocks
|
|
832
|
+
/// contribute nothing).
|
|
833
|
+
pub(super) fn message_text(message: &ChatMessage) -> String {
|
|
834
|
+
message
|
|
835
|
+
.content
|
|
836
|
+
.iter()
|
|
837
|
+
.filter_map(|block| match block {
|
|
838
|
+
ContentBlock::Text { text } => Some(text.as_str()),
|
|
839
|
+
_ => None,
|
|
840
|
+
})
|
|
841
|
+
.collect::<Vec<_>>()
|
|
842
|
+
.join("")
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/// Splits a `data:<mime>;base64,<data>` URI into `(mime_type, base64_data)`,
|
|
846
|
+
/// validating the mime type starts with `expected_prefix` (e.g. `"image/"`).
|
|
847
|
+
pub(super) fn split_data_uri(
|
|
848
|
+
data_uri: &str,
|
|
849
|
+
expected_prefix: &str,
|
|
850
|
+
) -> Result<(String, String), ChatError> {
|
|
851
|
+
data_uri
|
|
852
|
+
.strip_prefix("data:")
|
|
853
|
+
.and_then(|payload| payload.split_once(";base64,"))
|
|
854
|
+
.filter(|(mime, data)| mime.starts_with(expected_prefix) && !data.is_empty())
|
|
855
|
+
.map(|(mime, data)| (mime.to_owned(), data.to_owned()))
|
|
856
|
+
.ok_or(ChatError::InvalidAttachment)
|
|
857
|
+
}
|