@rexvane/piweb 0.3.20 → 0.3.22
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +6 -4
- package/.next/build-manifest.json +5 -5
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +7 -7
- package/.next/react-loadable-manifest.json +12 -12
- package/.next/required-server-files.js +18 -2
- package/.next/required-server-files.json +18 -2
- package/.next/routes-manifest.json +12 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.meta +8 -0
- package/.next/server/app/_global-error.rsc +15 -11
- package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +17 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -2
- package/.next/server/app/api/agent/[id]/events/route.js +3 -3
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/fs/route.js +1 -1
- package/.next/server/app/api/fs/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +4 -4
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/github/route.js +5 -0
- package/.next/server/app/api/github/route.js.nft.json +1 -0
- package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/health/route.js.nft.json +1 -1
- package/.next/server/app/api/models/defaults/route.js +1 -0
- package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
- package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/remote/route.js +3 -3
- package/.next/server/app/api/remote/route.js.nft.json +1 -1
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +2 -2
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +1 -1
- package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/icon.svg/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg.meta +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.meta +8 -0
- package/.next/server/app/index.rsc +19 -13
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
- package/.next/server/app/index.segments/_full.segment.rsc +19 -13
- package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.meta +8 -0
- package/.next/server/app/login.rsc +23 -17
- package/.next/server/app/login.segments/_full.segment.rsc +23 -17
- package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +6 -4
- package/.next/server/chunks/229.js +1 -1
- package/.next/server/chunks/421.js +1 -0
- package/.next/server/chunks/430.js +215 -31
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/662.js +2 -0
- package/.next/server/chunks/686.js +1 -1
- package/.next/server/chunks/807.js +1 -1
- package/.next/server/chunks/813.js +23 -1
- package/.next/server/chunks/920.js +2 -2
- package/.next/server/chunks/924.js +1 -1
- package/.next/server/chunks/970.js +305 -0
- package/.next/server/instrumentation.js +1 -1
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/middleware.js +185 -23
- package/.next/server/middleware.js.nft.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +5 -0
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
- package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
- package/.next/static/chunks/3794-0978865cda773008.js +52 -0
- package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
- package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
- package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
- package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
- package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
- package/.next/static/chunks/618.949981da99be7182.js +4 -0
- package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
- package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
- package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
- package/.next/static/css/6671d0d1589fdc0e.css +1 -0
- package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
- package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
- package/.next/types/app/api/agent/[id]/route.ts +3 -1
- package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
- package/.next/types/app/api/agent/new/route.ts +3 -1
- package/.next/types/app/api/config/route.ts +3 -1
- package/.next/types/app/api/files/route.ts +3 -1
- package/.next/types/app/api/fs/route.ts +3 -1
- package/.next/types/app/api/git/route.ts +3 -1
- package/.next/types/app/api/github/route.ts +355 -0
- package/.next/types/app/api/growth/route.ts +3 -1
- package/.next/types/app/api/health/route.ts +3 -1
- package/.next/types/app/api/models/defaults/route.ts +355 -0
- package/.next/types/app/api/models/route.ts +3 -1
- package/.next/types/app/api/pi-settings/route.ts +3 -1
- package/.next/types/app/api/plugins/route.ts +3 -1
- package/.next/types/app/api/prompts/route.ts +3 -1
- package/.next/types/app/api/remote/route.ts +3 -1
- package/.next/types/app/api/security/route.ts +3 -1
- package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
- package/.next/types/app/api/sessions/[id]/route.ts +3 -1
- package/.next/types/app/api/sessions/import/route.ts +3 -1
- package/.next/types/app/api/sessions/route.ts +3 -1
- package/.next/types/app/api/skills/route.ts +3 -1
- package/.next/types/app/api/update/route.ts +3 -1
- package/.next/types/app/api/version/route.ts +3 -1
- package/.next/types/app/api/web-auth/route.ts +3 -1
- package/.next/types/app/api/workspaces/route.ts +3 -1
- package/.next/types/app/layout.ts +4 -1
- package/.next/types/app/login/page.ts +4 -1
- package/.next/types/app/page.ts +4 -1
- package/.next/types/cache-life.d.ts +11 -43
- package/.next/types/routes.d.ts +3 -1
- package/.next/types/validator.ts +76 -2
- package/README.md +14 -5
- package/next.config.ts +4 -2
- package/package.json +2 -2
- package/scripts/release.mjs +1 -1
- package/src/app/api/agent/[id]/events/route.ts +42 -13
- package/src/app/api/files/route.ts +5 -2
- package/src/app/api/github/route.ts +26 -0
- package/src/app/api/models/defaults/route.ts +18 -0
- package/src/app/api/models/route.ts +24 -1
- package/src/app/api/update/route.ts +18 -2
- package/src/app/globals.css +275 -0
- package/src/components/AppShell.tsx +89 -33
- package/src/components/ChatInput.tsx +245 -63
- package/src/components/ChatWindow.tsx +123 -108
- package/src/components/ContextMeter.tsx +204 -123
- package/src/components/DrawerPanel.tsx +19 -26
- package/src/components/ExtensionUI.tsx +8 -2
- package/src/components/GitPanel.tsx +110 -40
- package/src/components/MarkdownBlock.tsx +333 -37
- package/src/components/ProjectPanel.tsx +55 -30
- package/src/components/PromptPanel.tsx +2 -4
- package/src/components/ProviderSetupModal.module.css +20 -0
- package/src/components/ProviderSetupModal.tsx +134 -29
- package/src/components/SettingsPanel.tsx +365 -80
- package/src/components/TrajectoryView.tsx +4 -6
- package/src/components/chat/chat-scroll.ts +18 -0
- package/src/components/composer/ComposerImages.tsx +306 -0
- package/src/components/update/CopyCommand.tsx +34 -0
- package/src/components/update/UpdateNotice.tsx +130 -0
- package/src/components/update/update-store.ts +155 -0
- package/src/hooks/useGrowth.ts +3 -0
- package/src/hooks/usePiWeb.ts +54 -27
- package/src/hooks/useWorkspaceFiles.ts +93 -0
- package/src/i18n.tsx +182 -6
- package/src/instrumentation-node.ts +3 -0
- package/src/lib/agent-manager.ts +193 -119
- package/src/lib/chat/event-stream.ts +113 -0
- package/src/lib/chat/stream-settings.ts +192 -0
- package/src/lib/files-service.ts +100 -1
- package/src/lib/format.ts +56 -0
- package/src/lib/git-service.ts +45 -13
- package/src/lib/github-identity.ts +245 -0
- package/src/lib/growth-service.ts +6 -2
- package/src/lib/hero-model-memory.ts +28 -0
- package/src/lib/model-defaults.ts +105 -0
- package/src/lib/model-draft.ts +257 -9
- package/src/lib/model-endpoint.ts +23 -0
- package/src/lib/model-files.ts +33 -0
- package/src/lib/models-service.ts +240 -78
- package/src/lib/pi.ts +27 -2
- package/src/lib/process-format.ts +3 -2
- package/src/lib/session/metrics.ts +31 -0
- package/src/lib/thinking-level.ts +39 -0
- package/src/lib/trajectory.ts +17 -3
- package/src/lib/types.ts +19 -1
- package/src/lib/update/startup-check.ts +117 -0
- package/src/lib/update-service.ts +23 -3
- package/src/lib/workspace-index.ts +270 -0
- package/.next/server/chunks/131.js +0 -1
- package/.next/server/chunks/243.js +0 -1
- package/.next/server/chunks/741.js +0 -165
- package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.js +0 -1
- package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
- package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
- package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
- package/.next/static/chunks/4686-b1e9fa876d0ea671.js +0 -1
- package/.next/static/chunks/4778.e4c9ed093a413bc4.js +0 -1
- package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
- package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
- package/.next/static/chunks/839.d3fe337a9117c325.js +0 -1
- package/.next/static/chunks/8552.6bfe3e101250b20f.js +0 -1
- package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/page-2e425ef83fba0782.js +0 -1
- package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
- package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/webpack-3614a8337843456b.js +0 -1
- package/.next/static/css/7391c42e1ae617f9.css +0 -1
- /package/.next/static/{_Y3Lq0lI5RJ1tFSOnDi0L → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
|
@@ -12,9 +12,10 @@ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
|
12
12
|
import { getNodeValue, parseTree, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
13
13
|
import { filterModelsByPiScope } from "./models/model-scope";
|
|
14
14
|
import { invalidateJsonFilesNamed, readJsonObjectSafe } from "./agent-config";
|
|
15
|
-
import { getAgentDir, getModelRuntime, refreshModelRuntime } from "./pi";
|
|
15
|
+
import { getAgentDir, getModelRuntime, modelRuntimeVersion, refreshModelRuntime, refreshModelRuntimeIfFilesChanged } from "./pi";
|
|
16
16
|
import { getEnabledModelPatterns } from "./pi-settings";
|
|
17
|
-
import { reloadSessionsForCwd } from "./agent-manager";
|
|
17
|
+
import { refreshSessionModels, reloadSessionsForCwd } from "./agent-manager";
|
|
18
|
+
import { withContextWindow } from "./model-draft";
|
|
18
19
|
import {
|
|
19
20
|
getSupportedThinkingLevels, InMemoryCredentialStore, InMemoryModelsStore,
|
|
20
21
|
type AuthInteraction, type AuthPrompt, type AuthType,
|
|
@@ -43,6 +44,7 @@ export interface ProviderView {
|
|
|
43
44
|
baseUrl?: string;
|
|
44
45
|
apiKeyLabel?: string;
|
|
45
46
|
authTypes: Array<"api_key" | "oauth">;
|
|
47
|
+
/** 用到的协议,用的模型最多的在前:apis[0] 是这家的默认协议(新加模型的 api、探测目录时的鉴权方式) */
|
|
46
48
|
apis: string[];
|
|
47
49
|
builtIn: boolean;
|
|
48
50
|
authConfigured: boolean;
|
|
@@ -60,13 +62,57 @@ export interface ProviderView {
|
|
|
60
62
|
export interface DiscoveredModel {
|
|
61
63
|
id: string;
|
|
62
64
|
name?: string;
|
|
65
|
+
/** 端点自报的上下文窗口(各家字段名不同,见 catalogCapabilities) */
|
|
66
|
+
contextWindow?: number;
|
|
67
|
+
/** 端点自报的单次输出上限 */
|
|
68
|
+
maxTokens?: number;
|
|
69
|
+
/** 端点自报的输入模态里有 image */
|
|
70
|
+
vision?: boolean;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
function positiveInt(value: unknown): number | undefined {
|
|
74
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 从模型目录条目里挑出能力字段。各家命名不同,能给就读、读不到就留空(不猜,缺的由体检卡片提醒用户填):
|
|
79
|
+
* - 上下文:`context_window`(本机中转)/ `context_length`(OpenRouter)/ `max_input_tokens`;
|
|
80
|
+
* - 输出上限:`max_output_tokens` / `max_completion_tokens` / `top_provider.max_completion_tokens`;
|
|
81
|
+
* - 视觉:`modalities.input`(本机中转)/ `architecture.input_modalities`(OpenRouter / llama.cpp)。
|
|
82
|
+
*/
|
|
83
|
+
export function catalogCapabilities(value: Record<string, unknown>): Pick<DiscoveredModel, "contextWindow" | "maxTokens" | "vision"> {
|
|
84
|
+
const architecture = isRecord(value.architecture) ? value.architecture : undefined;
|
|
85
|
+
const modalities = value.modalities ?? architecture?.input_modalities ?? value.input_modalities;
|
|
86
|
+
const inputList = Array.isArray(modalities)
|
|
87
|
+
? modalities
|
|
88
|
+
: isRecord(modalities) && Array.isArray(modalities.input)
|
|
89
|
+
? modalities.input
|
|
90
|
+
: undefined;
|
|
91
|
+
const topProvider = isRecord(value.top_provider) ? value.top_provider : undefined;
|
|
92
|
+
const contextWindow = positiveInt(value.context_window)
|
|
93
|
+
?? positiveInt(value.context_length)
|
|
94
|
+
?? positiveInt(value.max_input_tokens)
|
|
95
|
+
?? positiveInt(architecture?.context_length);
|
|
96
|
+
const maxTokens = positiveInt(value.max_output_tokens)
|
|
97
|
+
?? positiveInt(value.max_completion_tokens)
|
|
98
|
+
?? positiveInt(topProvider?.max_completion_tokens);
|
|
99
|
+
const vision = inputList
|
|
100
|
+
? inputList.some((item) => typeof item === "string" && item.toLowerCase() === "image")
|
|
101
|
+
: undefined;
|
|
102
|
+
return {
|
|
103
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
104
|
+
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
105
|
+
...(vision === undefined ? {} : { vision }),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let builtInCatalogPromise: Promise<Map<string, Map<string, number>>> | null = null;
|
|
66
110
|
|
|
67
111
|
/** 目录短缓存:页面挂载与设置关闭都会拉一次;认证探测(每个供应商一次网络往返)不必秒级重复 */
|
|
68
112
|
let listCache: { at: number; value: Promise<{ providers: ProviderView[]; models: ModelView[] }> } | null = null;
|
|
69
113
|
const LIST_TTL_MS = 5_000;
|
|
114
|
+
/** 缓存是按哪一代共享运行时算的:运行时刷新过(手改文件、/reload)就整体作废 */
|
|
115
|
+
let listedGeneration = -1;
|
|
70
116
|
|
|
71
117
|
/**
|
|
72
118
|
* 认证探测结果单独缓存:目录只活 5 秒,但「这个 provider 能不能用」在一次会话里几乎不变。
|
|
@@ -88,31 +134,55 @@ export function modelListStamp(): string {
|
|
|
88
134
|
return listCache ? String(listCache.at) : "none";
|
|
89
135
|
}
|
|
90
136
|
|
|
91
|
-
/** Read the unmodified SDK catalog through its public modelsPath:null option. */
|
|
92
|
-
function
|
|
93
|
-
if (!
|
|
94
|
-
|
|
137
|
+
/** Read the unmodified SDK catalog through its public modelsPath:null option: provider ID → model ID → built-in context window. */
|
|
138
|
+
function getBuiltInCatalog(): Promise<Map<string, Map<string, number>>> {
|
|
139
|
+
if (!builtInCatalogPromise) {
|
|
140
|
+
builtInCatalogPromise = ModelRuntime.create({
|
|
95
141
|
modelsPath: null, refreshOnCreate: false, allowModelNetwork: false,
|
|
96
142
|
credentials: new InMemoryCredentialStore(), modelsStore: new InMemoryModelsStore(),
|
|
97
143
|
})
|
|
98
|
-
.then((runtime) => new
|
|
144
|
+
.then((runtime) => new Map(runtime.getProviders().map((provider) => [
|
|
145
|
+
provider.id,
|
|
146
|
+
new Map(runtime.getModels(provider.id).map((model) => [model.id, Number((model as { contextWindow?: unknown }).contextWindow) || 0])),
|
|
147
|
+
])))
|
|
99
148
|
.catch((error) => {
|
|
100
|
-
|
|
149
|
+
builtInCatalogPromise = null;
|
|
101
150
|
throw error;
|
|
102
151
|
});
|
|
103
152
|
}
|
|
104
|
-
return
|
|
153
|
+
return builtInCatalogPromise;
|
|
105
154
|
}
|
|
106
155
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
/**
|
|
157
|
+
* 内置 provider 的目录模型 ID(provider → 排好序的 ID),给设置页的模型表单分流:与目录同 ID 的行写
|
|
158
|
+
* modelOverrides(只覆盖填写的字段),新 ID 才写 models[]——同 ID 的 models[] 条目会被 SDK 按默认值整条重建。
|
|
159
|
+
* 目录 = SDK 静态目录 ∪ 当前目录里不是 models.json `models[]` 定义出来的模型(远程目录缓存、OAuth / 扩展补的)。
|
|
160
|
+
* content 是 readCustomProviders 读到的那份,models 是同一时刻的完整目录。
|
|
161
|
+
*/
|
|
162
|
+
export async function builtinCatalogModelIds(content: string, models: ReadonlyArray<{ provider: string; id: string }>): Promise<Record<string, string[]>> {
|
|
163
|
+
const catalog = await getBuiltInCatalog();
|
|
164
|
+
const defined = new Map<string, Set<string>>();
|
|
165
|
+
for (const [providerId, provider] of Object.entries(parseCustomProviders(content).providers)) {
|
|
166
|
+
const rows = Array.isArray(provider.models) ? provider.models : [];
|
|
167
|
+
defined.set(providerId, new Set(rows.flatMap((row) => (isRecord(row) && typeof row.id === "string" ? [row.id] : []))));
|
|
168
|
+
}
|
|
169
|
+
const ids = new Map([...catalog].map(([providerId, modelIds]) => [providerId, new Set(modelIds.keys())]));
|
|
170
|
+
for (const model of models) {
|
|
171
|
+
if (!defined.get(model.provider)?.has(model.id)) ids.get(model.provider)?.add(model.id);
|
|
172
|
+
}
|
|
173
|
+
return Object.fromEntries([...ids].map(([providerId, modelIds]) => [providerId, [...modelIds].sort()]));
|
|
110
174
|
}
|
|
111
175
|
|
|
112
|
-
export function listModels(): Promise<{
|
|
176
|
+
export async function listModels(): Promise<{
|
|
113
177
|
providers: ProviderView[];
|
|
114
178
|
models: ModelView[];
|
|
115
179
|
}> {
|
|
180
|
+
// 手改过 models.json / auth.json 时先刷新共享目录(只做两次 stat),再看缓存是不是还对得上
|
|
181
|
+
await refreshModelRuntimeIfFilesChanged();
|
|
182
|
+
if (listedGeneration !== modelRuntimeVersion()) {
|
|
183
|
+
listedGeneration = modelRuntimeVersion();
|
|
184
|
+
invalidateModelList();
|
|
185
|
+
}
|
|
116
186
|
if (listCache && Date.now() - listCache.at < LIST_TTL_MS) return listCache.value;
|
|
117
187
|
const value = listModelsUncached().catch((error) => {
|
|
118
188
|
listCache = null;
|
|
@@ -136,7 +206,20 @@ export async function listModelsForPiWeb(): Promise<{ providers: ProviderView[];
|
|
|
136
206
|
};
|
|
137
207
|
}
|
|
138
208
|
|
|
139
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* provider 用到的协议,按用它的模型数从多到少排(同数按字母序)。apis[0] 当这家的默认协议用:
|
|
211
|
+
* 以前取字母序第一个,多协议的 provider 会挑到少数派(比如大部分模型走 responses 却给新模型配上 completions)。
|
|
212
|
+
*/
|
|
213
|
+
export function apisByUsage(models: ReadonlyArray<{ api?: unknown }>): string[] {
|
|
214
|
+
const counts = new Map<string, number>();
|
|
215
|
+
for (const model of models) {
|
|
216
|
+
const api = String(model.api ?? "");
|
|
217
|
+
if (api) counts.set(api, (counts.get(api) ?? 0) + 1);
|
|
218
|
+
}
|
|
219
|
+
return [...counts].sort(([a, x], [b, y]) => y - x || (a < b ? -1 : a > b ? 1 : 0)).map(([api]) => api);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** auth.json 各条目的凭证类型(顶层和 providers 两层都认);读取失败不影响目录 */
|
|
140
223
|
async function readStoredAuthTypes(): Promise<Map<string, "api_key" | "oauth">> {
|
|
141
224
|
const out = new Map<string, "api_key" | "oauth">();
|
|
142
225
|
try {
|
|
@@ -161,7 +244,7 @@ async function listModelsUncached(): Promise<{
|
|
|
161
244
|
models: ModelView[];
|
|
162
245
|
}> {
|
|
163
246
|
const rt = await getModelRuntime();
|
|
164
|
-
const
|
|
247
|
+
const builtInCatalog = await getBuiltInCatalog();
|
|
165
248
|
const providers = rt.getProviders() as any[];
|
|
166
249
|
const storedAuthTypes = await readStoredAuthTypes();
|
|
167
250
|
const out: ProviderView[] = [];
|
|
@@ -214,8 +297,8 @@ async function listModelsUncached(): Promise<{
|
|
|
214
297
|
baseUrl: typeof p.baseUrl === "string" ? p.baseUrl : undefined,
|
|
215
298
|
apiKeyLabel: typeof p.auth?.apiKey?.name === "string" ? p.auth.apiKey.name : undefined,
|
|
216
299
|
authTypes,
|
|
217
|
-
apis:
|
|
218
|
-
builtIn:
|
|
300
|
+
apis: apisByUsage(list),
|
|
301
|
+
builtIn: builtInCatalog.has(pid),
|
|
219
302
|
authConfigured: auth?.configured === true,
|
|
220
303
|
authSource: auth?.source,
|
|
221
304
|
authReady,
|
|
@@ -265,28 +348,46 @@ export async function removeApiKey(providerId: string): Promise<void> {
|
|
|
265
348
|
invalidateModelList();
|
|
266
349
|
}
|
|
267
350
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
351
|
+
interface ConfiguredCredential {
|
|
352
|
+
key: string;
|
|
353
|
+
/** 来自 OAuth 令牌(部分端点只认它,部分端点不认它) */
|
|
354
|
+
oauth: boolean;
|
|
355
|
+
/** openai-codex:ChatGPT 账号 ID,用量端点要带 */
|
|
356
|
+
accountId?: string;
|
|
357
|
+
/** 认证结果自带的地址(如 Copilot 企业版),也算这家已配置的地址 */
|
|
358
|
+
baseUrl?: string;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 该 provider 当前生效的凭证,和 SDK 发请求时同一套解析(ModelRuntime.getAuth):运行时 key → auth.json
|
|
363
|
+
* (OAuth 快过期会先刷新并写回)→ models.json 的 apiKey($ENV / ${ENV} / !command 照样解析)→ 环境变量。
|
|
364
|
+
* 编辑表单的 key 框留空(「留空保持不变」)时的模型目录探测、各家用量探测都从这里取。
|
|
365
|
+
* 拿不到或解析失败都返回 undefined:报错里可能带着命令原文,不往外带。
|
|
366
|
+
*/
|
|
367
|
+
async function configuredCredentialFor(providerId: string): Promise<ConfiguredCredential | undefined> {
|
|
279
368
|
try {
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
const key =
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
369
|
+
const rt = await getModelRuntime();
|
|
370
|
+
const resolved = await rt.getAuth(providerId, { signal: AbortSignal.timeout(20_000) });
|
|
371
|
+
const key = (resolved?.auth.apiKey ?? bearerToken(resolved?.auth.headers))?.trim();
|
|
372
|
+
if (!resolved || !key) return undefined;
|
|
373
|
+
const oauth = (await rt.checkAuth(providerId).catch(() => undefined))?.type === "oauth";
|
|
374
|
+
// SDK 发 Codex 请求时也是从令牌里取账号 ID
|
|
375
|
+
const accountId = oauth && providerId === "openai-codex" ? openaiAuthClaims(key)?.chatgpt_account_id : undefined;
|
|
376
|
+
return {
|
|
377
|
+
key,
|
|
378
|
+
oauth,
|
|
379
|
+
...(typeof accountId === "string" && accountId.trim() ? { accountId: accountId.trim() } : {}),
|
|
380
|
+
...(resolved.auth.baseUrl ? { baseUrl: resolved.auth.baseUrl } : {}),
|
|
381
|
+
};
|
|
288
382
|
} catch {
|
|
289
|
-
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** 只给请求头的认证结果(如 kimi-coding 的 OAuth):从 Authorization: Bearer 里取令牌 */
|
|
388
|
+
function bearerToken(headers: Record<string, string | null> | undefined): string | undefined {
|
|
389
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
390
|
+
if (name.toLowerCase() === "authorization" && value) return /^Bearer\s+(\S+)/i.exec(value.trim())?.[1];
|
|
290
391
|
}
|
|
291
392
|
return undefined;
|
|
292
393
|
}
|
|
@@ -424,18 +525,18 @@ export async function discoverModels(input: { baseUrl: string; api?: string; api
|
|
|
424
525
|
if (base.username || base.password) throw new Error("API 地址不能包含用户名或密码");
|
|
425
526
|
const endpoint = modelsEndpoint(base);
|
|
426
527
|
const headers: Record<string, string> = { Accept: "application/json" };
|
|
427
|
-
// 表单 key 为空时回退到该 provider
|
|
428
|
-
const credential = input.apiKey?.trim()
|
|
528
|
+
// 表单 key 为空时回退到该 provider 已配置的凭证(编辑场景:占位符「留空保持不变」)
|
|
529
|
+
const credential: ConfiguredCredential | undefined = input.apiKey?.trim()
|
|
429
530
|
? { key: input.apiKey.trim(), oauth: false }
|
|
430
531
|
: input.providerId
|
|
431
|
-
? await
|
|
532
|
+
? await configuredCredentialFor(input.providerId)
|
|
432
533
|
: undefined;
|
|
433
534
|
if (credential && !input.apiKey?.trim()) {
|
|
434
535
|
const rt = await getModelRuntime();
|
|
435
536
|
const provider = rt.getProviders().find((p) => p.id === input.providerId);
|
|
436
|
-
const configuredUrls = [provider?.baseUrl, ...(provider ? rt.getModels(provider.id).map((model) => model.baseUrl) : [])];
|
|
537
|
+
const configuredUrls = [provider?.baseUrl, credential.baseUrl, ...(provider ? rt.getModels(provider.id).map((model) => model.baseUrl) : [])];
|
|
437
538
|
if (!isTrustedCredentialEndpoint(endpoint, configuredUrls)) {
|
|
438
|
-
throw new Error("
|
|
539
|
+
throw new Error("已配置的凭证只能用于该 Provider 已配置的 API 地址;新地址请显式填写密钥");
|
|
439
540
|
}
|
|
440
541
|
}
|
|
441
542
|
const apiKey = credential?.key;
|
|
@@ -484,7 +585,7 @@ export async function discoverModels(input: { baseUrl: string; api?: string; api
|
|
|
484
585
|
const id = typeof value.id === "string" ? value.id.trim() : "";
|
|
485
586
|
if (!id) return null;
|
|
486
587
|
const name = typeof value.name === "string" && value.name.trim() ? value.name.trim() : undefined;
|
|
487
|
-
return { id, ...(name ? { name } : {}) };
|
|
588
|
+
return { id, ...(name ? { name } : {}), ...catalogCapabilities(value) };
|
|
488
589
|
})
|
|
489
590
|
.filter((model): model is DiscoveredModel => Boolean(model))
|
|
490
591
|
.filter((model, index, all) => all.findIndex((item) => item.id === model.id) === index)
|
|
@@ -586,17 +687,22 @@ function isoOrUndefined(value: unknown): string | undefined {
|
|
|
586
687
|
return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : undefined;
|
|
587
688
|
}
|
|
588
689
|
|
|
589
|
-
/**
|
|
590
|
-
|
|
690
|
+
/** ChatGPT access token(JWT)里的 OpenAI 声明:套餐、账号 ID 都在这里 */
|
|
691
|
+
function openaiAuthClaims(accessToken: string): Record<string, any> | undefined {
|
|
591
692
|
try {
|
|
592
|
-
const payload = JSON.parse(Buffer.from(accessToken.split(".")[1] ?? "", "base64").toString("utf8")) as
|
|
593
|
-
|
|
594
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
693
|
+
const payload = JSON.parse(Buffer.from(accessToken.split(".")[1] ?? "", "base64").toString("utf8")) as unknown;
|
|
694
|
+
return asRecord(asRecord(payload)?.["https://api.openai.com/auth"]);
|
|
595
695
|
} catch {
|
|
596
696
|
return undefined;
|
|
597
697
|
}
|
|
598
698
|
}
|
|
599
699
|
|
|
700
|
+
/** 从 access token 的 JWT 本地读套餐声明:端点不可用时界面仍显示套餐。 */
|
|
701
|
+
export function planTypeFromAccessToken(accessToken: string): string | undefined {
|
|
702
|
+
const value = openaiAuthClaims(accessToken)?.chatgpt_plan_type;
|
|
703
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
600
706
|
/**
|
|
601
707
|
* /wham/usage 载荷里只取界面需要的部分。窗口组合随套餐不同:常见是 5 小时窗 +
|
|
602
708
|
* 7 天窗,有的套餐只有 7 天窗,Go 这类只有 30 天窗(secondary_window 直接是
|
|
@@ -633,7 +739,7 @@ export function parseCodexUsagePayload(value: unknown): Omit<QuotaUsage, "kind"
|
|
|
633
739
|
}
|
|
634
740
|
|
|
635
741
|
async function probeCodexUsage(): Promise<QuotaUsage | null> {
|
|
636
|
-
const credential = await
|
|
742
|
+
const credential = await configuredCredentialFor("openai-codex");
|
|
637
743
|
if (!credential) return null;
|
|
638
744
|
// JWT 里的套餐声明本地可得,端点失败时用它兜底,界面不至于整块空白
|
|
639
745
|
const fallbackPlan = planTypeFromAccessToken(credential.key);
|
|
@@ -684,7 +790,7 @@ export function parseAnthropicUsagePayload(value: unknown): Omit<QuotaUsage, "ki
|
|
|
684
790
|
}
|
|
685
791
|
|
|
686
792
|
async function probeAnthropicUsage(): Promise<QuotaUsage | null> {
|
|
687
|
-
const credential = await
|
|
793
|
+
const credential = await configuredCredentialFor("anthropic");
|
|
688
794
|
// 该端点只认订阅 OAuth 令牌;API key 走的是按量计费,没有这组窗口
|
|
689
795
|
if (!credential?.oauth) return null;
|
|
690
796
|
const payload = await fetchUsageJson(ANTHROPIC_USAGE_ENDPOINT, {
|
|
@@ -757,7 +863,7 @@ const MOONSHOT_HOSTS: Record<string, { base: string; currency: string }> = {
|
|
|
757
863
|
};
|
|
758
864
|
|
|
759
865
|
async function probeDeepSeekBalance(): Promise<BalanceUsage | null> {
|
|
760
|
-
const credential = await
|
|
866
|
+
const credential = await configuredCredentialFor("deepseek");
|
|
761
867
|
if (!credential) return null;
|
|
762
868
|
const payload = await fetchUsageJson("https://api.deepseek.com/user/balance", {
|
|
763
869
|
headers: { Authorization: `Bearer ${credential.key}` },
|
|
@@ -767,7 +873,7 @@ async function probeDeepSeekBalance(): Promise<BalanceUsage | null> {
|
|
|
767
873
|
}
|
|
768
874
|
|
|
769
875
|
async function probeOpenRouterBalance(): Promise<BalanceUsage | null> {
|
|
770
|
-
const credential = await
|
|
876
|
+
const credential = await configuredCredentialFor("openrouter");
|
|
771
877
|
if (!credential) return null;
|
|
772
878
|
const payload = await fetchUsageJson("https://openrouter.ai/api/v1/key", {
|
|
773
879
|
headers: { Authorization: `Bearer ${credential.key}` },
|
|
@@ -777,7 +883,7 @@ async function probeOpenRouterBalance(): Promise<BalanceUsage | null> {
|
|
|
777
883
|
}
|
|
778
884
|
|
|
779
885
|
async function probeMoonshotBalance(providerId: "moonshotai" | "moonshotai-cn"): Promise<BalanceUsage | null> {
|
|
780
|
-
const credential = await
|
|
886
|
+
const credential = await configuredCredentialFor(providerId);
|
|
781
887
|
if (!credential) return null;
|
|
782
888
|
const host = MOONSHOT_HOSTS[providerId];
|
|
783
889
|
const payload = await fetchUsageJson(`${host.base}/v1/users/me/balance`, {
|
|
@@ -894,7 +1000,7 @@ export function parseOpencodeUsagePayload(value: unknown): Omit<QuotaUsage, "kin
|
|
|
894
1000
|
}
|
|
895
1001
|
|
|
896
1002
|
async function probeKimiCodingUsage(): Promise<QuotaUsage | null> {
|
|
897
|
-
const credential = await
|
|
1003
|
+
const credential = await configuredCredentialFor("kimi-coding");
|
|
898
1004
|
if (!credential) return null;
|
|
899
1005
|
const payload = await fetchUsageJson("https://api.kimi.com/coding/v1/usages", {
|
|
900
1006
|
headers: { Authorization: `Bearer ${credential.key}`, Accept: "application/json" },
|
|
@@ -905,7 +1011,7 @@ async function probeKimiCodingUsage(): Promise<QuotaUsage | null> {
|
|
|
905
1011
|
}
|
|
906
1012
|
|
|
907
1013
|
async function probeCopilotUsage(): Promise<QuotaUsage | null> {
|
|
908
|
-
const credential = await
|
|
1014
|
+
const credential = await configuredCredentialFor("github-copilot");
|
|
909
1015
|
if (!credential?.oauth) return null;
|
|
910
1016
|
// 凭据本身就是 Copilot 令牌(tid=...),直接可用于这个内部端点
|
|
911
1017
|
const payload = await fetchUsageJson("https://api.github.com/copilot_internal/user", {
|
|
@@ -917,7 +1023,7 @@ async function probeCopilotUsage(): Promise<QuotaUsage | null> {
|
|
|
917
1023
|
}
|
|
918
1024
|
|
|
919
1025
|
async function probeVercelGatewayBalance(): Promise<BalanceUsage | null> {
|
|
920
|
-
const credential = await
|
|
1026
|
+
const credential = await configuredCredentialFor("vercel-ai-gateway");
|
|
921
1027
|
if (!credential) return null;
|
|
922
1028
|
const payload = await fetchUsageJson("https://ai-gateway.vercel.sh/v1/credits", {
|
|
923
1029
|
headers: { Authorization: `Bearer ${credential.key}` },
|
|
@@ -927,7 +1033,7 @@ async function probeVercelGatewayBalance(): Promise<BalanceUsage | null> {
|
|
|
927
1033
|
}
|
|
928
1034
|
|
|
929
1035
|
async function probeOpencodeUsage(): Promise<QuotaUsage | null> {
|
|
930
|
-
const credential = await
|
|
1036
|
+
const credential = await configuredCredentialFor("opencode-go");
|
|
931
1037
|
if (!credential) return null;
|
|
932
1038
|
const payload = await fetchUsageJson("https://opencode.ai/zen/go/v1/usage", {
|
|
933
1039
|
headers: { Authorization: `Bearer ${credential.key}`, Accept: "application/json" },
|
|
@@ -938,7 +1044,7 @@ async function probeOpencodeUsage(): Promise<QuotaUsage | null> {
|
|
|
938
1044
|
}
|
|
939
1045
|
|
|
940
1046
|
async function probeXaiUsage(): Promise<XaiUsage | null> {
|
|
941
|
-
const credential = await
|
|
1047
|
+
const credential = await configuredCredentialFor("xai");
|
|
942
1048
|
if (!credential) return null;
|
|
943
1049
|
const key = credential.key;
|
|
944
1050
|
let model = "";
|
|
@@ -1306,6 +1412,38 @@ async function validateStagedCustomProviders(file: string): Promise<void> {
|
|
|
1306
1412
|
if (error) throw new CustomProvidersValidationError(error);
|
|
1307
1413
|
}
|
|
1308
1414
|
|
|
1415
|
+
/**
|
|
1416
|
+
* 校验后原子替换 models.json(调用方已持两把锁):现有文件先过一遍 SDK 校验(坏文件不能当空文件覆盖),
|
|
1417
|
+
* 新内容校验通过、且期间没被别的程序改过才落盘。
|
|
1418
|
+
*/
|
|
1419
|
+
async function replaceCustomProvidersFile(
|
|
1420
|
+
file: string,
|
|
1421
|
+
previous: { content: string | undefined; revision: string },
|
|
1422
|
+
next: CustomProvidersFile,
|
|
1423
|
+
prepare?: (old: CustomProvidersFile) => void,
|
|
1424
|
+
): Promise<void> {
|
|
1425
|
+
const old = parseCustomProviders(previous.content ?? '{"providers":{}}');
|
|
1426
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
1427
|
+
try {
|
|
1428
|
+
if (previous.content !== undefined) {
|
|
1429
|
+
await fs.writeFile(temporary, JSON.stringify(old, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
1430
|
+
// A damaged existing config is never treated as an empty/missing file.
|
|
1431
|
+
await validateStagedCustomProviders(temporary);
|
|
1432
|
+
}
|
|
1433
|
+
prepare?.(old);
|
|
1434
|
+
validateTokenLimits(next);
|
|
1435
|
+
await fs.writeFile(temporary, JSON.stringify(next, null, 2), {
|
|
1436
|
+
encoding: "utf8", flag: previous.content === undefined ? "wx" : "w", mode: 0o600,
|
|
1437
|
+
});
|
|
1438
|
+
await validateStagedCustomProviders(temporary);
|
|
1439
|
+
// Also detect non-cooperating writers that changed the file during validation.
|
|
1440
|
+
if ((await readCustomProvidersSnapshot(file)).revision !== previous.revision) throw new CustomProvidersConflictError();
|
|
1441
|
+
await fs.rename(temporary, file);
|
|
1442
|
+
} finally {
|
|
1443
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1309
1447
|
export async function writeCustomProviders(content: string, revision?: string): Promise<void> {
|
|
1310
1448
|
const file = modelsJsonPath();
|
|
1311
1449
|
await withSettingsWriteLock(file, () => withExternalSettingsLock(file, async () => {
|
|
@@ -1313,28 +1451,11 @@ export async function writeCustomProviders(content: string, revision?: string):
|
|
|
1313
1451
|
// Compare before parsing or restoring secrets; stale clients must reload, not overwrite.
|
|
1314
1452
|
if (revision !== undefined && revision !== previous.revision) throw new CustomProvidersConflictError();
|
|
1315
1453
|
const parsed = parseCustomProviders(content);
|
|
1316
|
-
|
|
1317
|
-
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
1318
|
-
try {
|
|
1319
|
-
if (previous.content !== undefined) {
|
|
1320
|
-
await fs.writeFile(temporary, JSON.stringify(old, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
1321
|
-
// A damaged existing config is never treated as an empty/missing file.
|
|
1322
|
-
await validateStagedCustomProviders(temporary);
|
|
1323
|
-
}
|
|
1454
|
+
await replaceCustomProvidersFile(file, previous, parsed, (old) => {
|
|
1324
1455
|
mergeCustomProviderApiKeys(parsed, old);
|
|
1325
1456
|
// 本地网关无 key 也得给占位值,否则 SDK 判定「未配置」→ 发消息报 No API key
|
|
1326
1457
|
applyLocalPlaceholderKeys(parsed);
|
|
1327
|
-
|
|
1328
|
-
await fs.writeFile(temporary, JSON.stringify(parsed, null, 2), {
|
|
1329
|
-
encoding: "utf8", flag: previous.content === undefined ? "wx" : "w", mode: 0o600,
|
|
1330
|
-
});
|
|
1331
|
-
await validateStagedCustomProviders(temporary);
|
|
1332
|
-
// Also detect non-cooperating writers that changed the file during validation.
|
|
1333
|
-
if ((await readCustomProvidersSnapshot(file)).revision !== previous.revision) throw new CustomProvidersConflictError();
|
|
1334
|
-
await fs.rename(temporary, file);
|
|
1335
|
-
} finally {
|
|
1336
|
-
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
1337
|
-
}
|
|
1458
|
+
});
|
|
1338
1459
|
}));
|
|
1339
1460
|
await refreshModelRuntime();
|
|
1340
1461
|
invalidateModelList();
|
|
@@ -1342,3 +1463,44 @@ export async function writeCustomProviders(content: string, revision?: string):
|
|
|
1342
1463
|
// 把当前模型对象换成新配置(改了 baseUrl / contextWindow 等字段时生效)
|
|
1343
1464
|
await reloadSessionsForCwd();
|
|
1344
1465
|
}
|
|
1466
|
+
|
|
1467
|
+
export interface ContextWindowInfo {
|
|
1468
|
+
/** 当前生效的值(已套用 models.json) */
|
|
1469
|
+
contextWindow: number;
|
|
1470
|
+
/** pi 内置目录里的原值;不在内置目录里的模型(自定义、远程目录、扩展补的)为 null */
|
|
1471
|
+
builtin: number | null;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
async function contextWindowOf(providerId: string, modelId: string): Promise<ContextWindowInfo> {
|
|
1475
|
+
const model = (await getModelRuntime()).getModel(providerId, modelId) as { contextWindow?: unknown } | undefined;
|
|
1476
|
+
if (!model) throw new CustomProvidersValidationError(`找不到模型:${providerId}/${modelId}`);
|
|
1477
|
+
const builtin = (await getBuiltInCatalog()).get(providerId)?.get(modelId);
|
|
1478
|
+
return { contextWindow: Number(model.contextWindow) || 0, builtin: builtin || null };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/** 上下文圆环「修改窗口」用:当前值,加内置目录原值给用户对照(只对照,不替用户决定) */
|
|
1482
|
+
export async function contextWindowInfo(providerId: string, modelId: string): Promise<ContextWindowInfo> {
|
|
1483
|
+
await refreshModelRuntimeIfFilesChanged();
|
|
1484
|
+
return contextWindowOf(providerId, modelId);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* 只改一个模型的上下文窗口(写在 pi 实际取值的那一层,见 withContextWindow),与设置页共用校验和原子替换。
|
|
1489
|
+
* 不重载会话资源:刷新共享目录后,只给正在用这个模型的会话换上新模型对象并推一次用量,圆环当场更新。
|
|
1490
|
+
*/
|
|
1491
|
+
export async function setModelContextWindow(providerId: string, modelId: string, contextWindow: number): Promise<ContextWindowInfo> {
|
|
1492
|
+
if (!Number.isSafeInteger(contextWindow) || contextWindow <= 0) throw new CustomProvidersValidationError("contextWindow 必须是正整数");
|
|
1493
|
+
// 只给目录里有的模型写:未知 provider / ID 不落进配置
|
|
1494
|
+
await contextWindowOf(providerId, modelId);
|
|
1495
|
+
const file = modelsJsonPath();
|
|
1496
|
+
await withSettingsWriteLock(file, () => withExternalSettingsLock(file, async () => {
|
|
1497
|
+
const previous = await readCustomProvidersSnapshot(file);
|
|
1498
|
+
const parsed = parseCustomProviders(previous.content ?? '{"providers":{}}');
|
|
1499
|
+
const providers = withContextWindow(parsed.providers, providerId, modelId, contextWindow) as CustomProvidersFile["providers"];
|
|
1500
|
+
await replaceCustomProvidersFile(file, previous, { ...parsed, providers });
|
|
1501
|
+
}));
|
|
1502
|
+
await refreshModelRuntime();
|
|
1503
|
+
invalidateModelList();
|
|
1504
|
+
refreshSessionModels(providerId, modelId);
|
|
1505
|
+
return contextWindowOf(providerId, modelId);
|
|
1506
|
+
}
|
package/src/lib/pi.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
loadProjectContextFiles,
|
|
24
24
|
} from "@earendil-works/pi-coding-agent";
|
|
25
25
|
|
|
26
|
+
import { modelFilesFingerprint, reloadStoredCredentials } from "./model-files";
|
|
27
|
+
|
|
26
28
|
export { createAgentSession, getAgentDir, loadProjectContextFiles, SessionManager };
|
|
27
29
|
|
|
28
30
|
// Child tools inherit these defaults; Node itself already decodes source and JSON as UTF-8.
|
|
@@ -30,9 +32,15 @@ process.env.PYTHONUTF8 ??= "1";
|
|
|
30
32
|
process.env.PYTHONIOENCODING ??= "utf-8";
|
|
31
33
|
|
|
32
34
|
let modelRuntimePromise: Promise<PiModelRuntime> | null = null;
|
|
35
|
+
/** 共享运行时每创建一次、每刷新一次就加一:模型列表缓存据此作废(见 models-service) */
|
|
36
|
+
let modelRuntimeGeneration = 0;
|
|
37
|
+
/** 上一次加载目录时 models.json / auth.json 的指纹 */
|
|
38
|
+
let modelFilesSeen: string | null = null;
|
|
33
39
|
|
|
34
40
|
export function getModelRuntime(): Promise<PiModelRuntime> {
|
|
35
41
|
if (!modelRuntimePromise) {
|
|
42
|
+
modelFilesSeen = modelFilesFingerprint(getAgentDir());
|
|
43
|
+
modelRuntimeGeneration += 1;
|
|
36
44
|
modelRuntimePromise = ModelRuntime.create().catch((err) => {
|
|
37
45
|
modelRuntimePromise = null;
|
|
38
46
|
throw err;
|
|
@@ -41,22 +49,39 @@ export function getModelRuntime(): Promise<PiModelRuntime> {
|
|
|
41
49
|
return modelRuntimePromise;
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
export function modelRuntimeVersion(): number {
|
|
53
|
+
return modelRuntimeGeneration;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
/**
|
|
45
57
|
* models.json 落盘后调用:就地刷新共享的 ModelRuntime,让目录与认证立即生效。
|
|
46
58
|
* 不能换一个新实例 —— 活跃会话、扩展注册的 provider 都挂在旧实例上,换掉后会话里
|
|
47
59
|
* 选新 provider 的模型会报 "No API key",扩展 provider 也会从目录里消失。
|
|
48
|
-
*
|
|
60
|
+
* 只读本地配置不联网;刷新失败才退回整体重建。手改的 auth.json 也在这里重读(SDK 的存储会缓存它)。
|
|
49
61
|
*/
|
|
50
62
|
export async function refreshModelRuntime(): Promise<void> {
|
|
51
63
|
const pending = modelRuntimePromise;
|
|
52
64
|
if (!pending) return;
|
|
65
|
+
// 先记指纹再刷新:刷新期间又改的文件,下一次检查还能发现
|
|
66
|
+
modelFilesSeen = modelFilesFingerprint(getAgentDir());
|
|
67
|
+
modelRuntimeGeneration += 1;
|
|
53
68
|
try {
|
|
54
|
-
|
|
69
|
+
const runtime = await pending;
|
|
70
|
+
reloadStoredCredentials(runtime);
|
|
71
|
+
await runtime.refresh({ allowNetwork: false });
|
|
55
72
|
} catch {
|
|
56
73
|
if (modelRuntimePromise === pending) modelRuntimePromise = null;
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
/** 读目录的入口调用:models.json / auth.json 的指纹变了才刷新(两次 stat,不读内容)。返回是否刷新过。 */
|
|
78
|
+
export async function refreshModelRuntimeIfFilesChanged(): Promise<boolean> {
|
|
79
|
+
if (!modelRuntimePromise) return false;
|
|
80
|
+
if (modelFilesFingerprint(getAgentDir()) === modelFilesSeen) return false;
|
|
81
|
+
await refreshModelRuntime();
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
const settingsCache = new Map<string, SettingsManager>();
|
|
61
86
|
/** 缓存键 → 原始目录。键在 Windows 上是小写的,不能拿它当目录交给 SDK 或算资源指纹 */
|
|
62
87
|
const settingsCwds = new Map<string, string>();
|
|
@@ -86,11 +86,12 @@ export function classifyMessageChars(
|
|
|
86
86
|
if (content.type === "text") {
|
|
87
87
|
const chars = content.text?.length ?? 0;
|
|
88
88
|
if (message.role === "user") out.user += chars;
|
|
89
|
-
else out.
|
|
89
|
+
else if (message.role === "toolResult") out.toolOutput += chars;
|
|
90
|
+
else if (message.role !== "system") out.agentText += chars;
|
|
90
91
|
} else if (content.type === "thinking") {
|
|
91
92
|
out.agentThinking += content.thinking?.length ?? 0;
|
|
92
93
|
} else if (content.type === "toolCall") {
|
|
93
|
-
out.agentToolCall += JSON.stringify(content.arguments ?? "").length;
|
|
94
|
+
out.agentToolCall += ((content as { name?: string }).name?.length ?? 0) + JSON.stringify(content.arguments ?? "").length;
|
|
94
95
|
} else if (content.type === "toolResult") {
|
|
95
96
|
out.toolOutput += (content as { text?: string }).text?.length ?? 0;
|
|
96
97
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { calculateContextTokens, estimateTokens, type SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { WebSnapshot } from "../types";
|
|
3
|
+
|
|
4
|
+
/** 冷会话沿用 SDK 的投影口径:API usage + 尾部估算;压缩 / 编辑后不信旧 usage。 */
|
|
5
|
+
export function projectedContextUsage(
|
|
6
|
+
projection: ReturnType<SessionManager["buildSessionProjection"]>,
|
|
7
|
+
branch: ReturnType<SessionManager["getBranch"]>,
|
|
8
|
+
contextWindow: number,
|
|
9
|
+
): WebSnapshot["contextUsage"] {
|
|
10
|
+
if (contextWindow <= 0) return null;
|
|
11
|
+
let lastUsage: { tokens: number; messageIndex: number; branchIndex: number } | undefined;
|
|
12
|
+
let messageIndex = 0;
|
|
13
|
+
for (const entry of projection.entries) {
|
|
14
|
+
for (const message of entry.messages) {
|
|
15
|
+
if (message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted" && message.usage) {
|
|
16
|
+
const tokens = calculateContextTokens(message.usage);
|
|
17
|
+
if (tokens > 0) lastUsage = { tokens, messageIndex, branchIndex: branch.findIndex((raw) => raw.id === entry.sourceEntry.id) };
|
|
18
|
+
}
|
|
19
|
+
messageIndex++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const compactionIndex = branch.findLastIndex((entry) => entry.type === "compaction");
|
|
23
|
+
if (compactionIndex >= 0 && (!lastUsage || lastUsage.branchIndex <= compactionIndex)) {
|
|
24
|
+
return { tokens: null, contextWindow, percent: null };
|
|
25
|
+
}
|
|
26
|
+
const invalidatingIndex = branch.findLastIndex((entry) => entry.type === "compaction" || entry.type === "context_edit");
|
|
27
|
+
const tokens = lastUsage && lastUsage.branchIndex > invalidatingIndex
|
|
28
|
+
? lastUsage.tokens + projection.messages.slice(lastUsage.messageIndex + 1).reduce((sum, message) => sum + estimateTokens(message), 0)
|
|
29
|
+
: projection.messages.reduce((sum, message) => sum + estimateTokens(message), 0);
|
|
30
|
+
return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
|
|
31
|
+
}
|