@tonyclaw/agent-inspector 2.0.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/.output/cli.js +1611 -0
- package/.output/nitro.json +17 -0
- package/.output/public/assets/CompareDrawer-CU5ZrWcL.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-pEBqVp1d.js +101 -0
- package/.output/public/assets/ReplayDialog-F58yNg5j.js +1 -0
- package/.output/public/assets/RequestAnatomy-C9lT0qE_.js +1 -0
- package/.output/public/assets/ResponseView-DHJq6bnz.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-BTgfpFUT.js +1 -0
- package/.output/public/assets/_sessionId-DsNRbnNm.js +1 -0
- package/.output/public/assets/alibaba-TTwafVwX.svg +1 -0
- package/.output/public/assets/index-CpWG2hFn.css +1 -0
- package/.output/public/assets/index-DmBV8Gve.js +1 -0
- package/.output/public/assets/json-viewer-CZVYLR8j.js +14 -0
- package/.output/public/assets/main-DHs7FBK3.js +18 -0
- package/.output/public/assets/minimax-BPMzvuL-.jpeg +0 -0
- package/.output/public/assets/qwen-CONDcHqt.png +0 -0
- package/.output/public/assets/zhipuai-BPNAnxo-.svg +219 -0
- package/.output/server/_chunks/ssr-renderer.mjs +22 -0
- package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +210 -0
- package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +2 -0
- package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +2 -0
- package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +2 -0
- package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +11 -0
- package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +69 -0
- package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-escape-keydown+[...].mjs +17 -0
- package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +1 -0
- package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +6 -0
- package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +34 -0
- package/.output/server/_libs/ajv-formats.mjs +330 -0
- package/.output/server/_libs/ajv.mjs +11444 -0
- package/.output/server/_libs/aria-hidden.mjs +122 -0
- package/.output/server/_libs/atomically.mjs +152 -0
- package/.output/server/_libs/bail.mjs +8 -0
- package/.output/server/_libs/cfworker__json-schema.mjs +1 -0
- package/.output/server/_libs/character-entities.mjs +2130 -0
- package/.output/server/_libs/class-variance-authority.mjs +44 -0
- package/.output/server/_libs/clsx.mjs +16 -0
- package/.output/server/_libs/comma-separated-tokens.mjs +10 -0
- package/.output/server/_libs/conf.mjs +635 -0
- package/.output/server/_libs/cookie-es.mjs +58 -0
- package/.output/server/_libs/core-util-is.mjs +75 -0
- package/.output/server/_libs/croner.mjs +1 -0
- package/.output/server/_libs/crossws.mjs +1 -0
- package/.output/server/_libs/debounce-fn.mjs +69 -0
- package/.output/server/_libs/decode-named-character-reference+[...].mjs +8 -0
- package/.output/server/_libs/dequal.mjs +27 -0
- package/.output/server/_libs/detect-node-es.mjs +1 -0
- package/.output/server/_libs/devlop.mjs +8 -0
- package/.output/server/_libs/diff.mjs +320 -0
- package/.output/server/_libs/dot-prop.mjs +265 -0
- package/.output/server/_libs/env-paths.mjs +57 -0
- package/.output/server/_libs/estree-util-is-identifier-name.mjs +11 -0
- package/.output/server/_libs/extend.mjs +97 -0
- package/.output/server/_libs/fast-deep-equal.mjs +38 -0
- package/.output/server/_libs/fast-uri.mjs +812 -0
- package/.output/server/_libs/floating-ui__core.mjs +725 -0
- package/.output/server/_libs/floating-ui__dom.mjs +622 -0
- package/.output/server/_libs/floating-ui__react-dom.mjs +292 -0
- package/.output/server/_libs/floating-ui__utils.mjs +320 -0
- package/.output/server/_libs/get-nonce.mjs +9 -0
- package/.output/server/_libs/h3-v2.mjs +276 -0
- package/.output/server/_libs/h3.mjs +408 -0
- package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +388 -0
- package/.output/server/_libs/hast-util-whitespace.mjs +10 -0
- package/.output/server/_libs/hookable.mjs +1 -0
- package/.output/server/_libs/html-url-attributes.mjs +26 -0
- package/.output/server/_libs/immediate.mjs +74 -0
- package/.output/server/_libs/inherits.mjs +50 -0
- package/.output/server/_libs/inline-style-parser.mjs +142 -0
- package/.output/server/_libs/is-plain-obj.mjs +10 -0
- package/.output/server/_libs/isarray.mjs +14 -0
- package/.output/server/_libs/isbot.mjs +20 -0
- package/.output/server/_libs/json-schema-traverse.mjs +180 -0
- package/.output/server/_libs/jszip.mjs +3051 -0
- package/.output/server/_libs/lie.mjs +273 -0
- package/.output/server/_libs/lucide-react.mjs +492 -0
- package/.output/server/_libs/mdast-util-from-markdown.mjs +717 -0
- package/.output/server/_libs/mdast-util-to-hast.mjs +710 -0
- package/.output/server/_libs/mdast-util-to-string.mjs +38 -0
- package/.output/server/_libs/micromark-core-commonmark.mjs +2259 -0
- package/.output/server/_libs/micromark-factory-destination.mjs +94 -0
- package/.output/server/_libs/micromark-factory-label.mjs +63 -0
- package/.output/server/_libs/micromark-factory-space.mjs +24 -0
- package/.output/server/_libs/micromark-factory-title.mjs +65 -0
- package/.output/server/_libs/micromark-factory-whitespace.mjs +22 -0
- package/.output/server/_libs/micromark-util-character.mjs +44 -0
- package/.output/server/_libs/micromark-util-chunked.mjs +36 -0
- package/.output/server/_libs/micromark-util-classify-character+[...].mjs +12 -0
- package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +41 -0
- package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +19 -0
- package/.output/server/_libs/micromark-util-decode-string.mjs +21 -0
- package/.output/server/_libs/micromark-util-encode.mjs +1 -0
- package/.output/server/_libs/micromark-util-html-tag-name.mjs +69 -0
- package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +6 -0
- package/.output/server/_libs/micromark-util-resolve-all.mjs +15 -0
- package/.output/server/_libs/micromark-util-sanitize-uri.mjs +41 -0
- package/.output/server/_libs/micromark-util-subtokenize.mjs +346 -0
- package/.output/server/_libs/micromark.mjs +906 -0
- package/.output/server/_libs/mimic-function.mjs +47 -0
- package/.output/server/_libs/modelcontextprotocol__server.mjs +9738 -0
- package/.output/server/_libs/ocache.mjs +1 -0
- package/.output/server/_libs/ohash.mjs +1 -0
- package/.output/server/_libs/pako.mjs +4223 -0
- package/.output/server/_libs/process-nextick-args.mjs +48 -0
- package/.output/server/_libs/property-information.mjs +1209 -0
- package/.output/server/_libs/radix-ui.mjs +1 -0
- package/.output/server/_libs/radix-ui__number.mjs +6 -0
- package/.output/server/_libs/radix-ui__primitive.mjs +11 -0
- package/.output/server/_libs/radix-ui__react-accordion.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-arrow.mjs +23 -0
- package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-avatar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-checkbox.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-collapsible.mjs +144 -0
- package/.output/server/_libs/radix-ui__react-collection.mjs +69 -0
- package/.output/server/_libs/radix-ui__react-compose-refs.mjs +39 -0
- package/.output/server/_libs/radix-ui__react-context-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-context.mjs +78 -0
- package/.output/server/_libs/radix-ui__react-dialog.mjs +325 -0
- package/.output/server/_libs/radix-ui__react-direction.mjs +9 -0
- package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-focus-guards.mjs +29 -0
- package/.output/server/_libs/radix-ui__react-focus-scope.mjs +206 -0
- package/.output/server/_libs/radix-ui__react-form.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-hover-card.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-id.mjs +14 -0
- package/.output/server/_libs/radix-ui__react-label.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-menu.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-menubar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-popover.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-popper.mjs +286 -0
- package/.output/server/_libs/radix-ui__react-portal.mjs +16 -0
- package/.output/server/_libs/radix-ui__react-presence.mjs +128 -0
- package/.output/server/_libs/radix-ui__react-primitive.mjs +42 -0
- package/.output/server/_libs/radix-ui__react-progress.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-radio-group.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-roving-focus.mjs +224 -0
- package/.output/server/_libs/radix-ui__react-scroll-area.mjs +721 -0
- package/.output/server/_libs/radix-ui__react-select.mjs +1163 -0
- package/.output/server/_libs/radix-ui__react-separator.mjs +28 -0
- package/.output/server/_libs/radix-ui__react-slider.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-slot.mjs +99 -0
- package/.output/server/_libs/radix-ui__react-switch.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-tabs.mjs +189 -0
- package/.output/server/_libs/radix-ui__react-toast.mjs +2 -0
- package/.output/server/_libs/radix-ui__react-toggle-group.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-toggle.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-toolbar.mjs +1 -0
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +495 -0
- package/.output/server/_libs/radix-ui__react-use-previous.mjs +14 -0
- package/.output/server/_libs/radix-ui__react-use-size.mjs +39 -0
- package/.output/server/_libs/react-dom.mjs +10781 -0
- package/.output/server/_libs/react-markdown.mjs +147 -0
- package/.output/server/_libs/react-remove-scroll-bar.mjs +82 -0
- package/.output/server/_libs/react-remove-scroll.mjs +328 -0
- package/.output/server/_libs/react-style-singleton.mjs +69 -0
- package/.output/server/_libs/react.mjs +515 -0
- package/.output/server/_libs/readable-stream.mjs +1518 -0
- package/.output/server/_libs/remark-parse.mjs +19 -0
- package/.output/server/_libs/remark-rehype.mjs +21 -0
- package/.output/server/_libs/rou3.mjs +14 -0
- package/.output/server/_libs/safe-buffer.mjs +64 -0
- package/.output/server/_libs/semver.mjs +1938 -0
- package/.output/server/_libs/seroval-plugins.mjs +58 -0
- package/.output/server/_libs/seroval.mjs +1765 -0
- package/.output/server/_libs/setimmediate.mjs +152 -0
- package/.output/server/_libs/space-separated-tokens.mjs +6 -0
- package/.output/server/_libs/srvx.mjs +1029 -0
- package/.output/server/_libs/stubborn-fs.mjs +91 -0
- package/.output/server/_libs/stubborn-utils.mjs +66 -0
- package/.output/server/_libs/style-to-js.mjs +72 -0
- package/.output/server/_libs/style-to-object.mjs +38 -0
- package/.output/server/_libs/swr.mjs +939 -0
- package/.output/server/_libs/tailwind-merge.mjs +3010 -0
- package/.output/server/_libs/tanstack__history.mjs +217 -0
- package/.output/server/_libs/tanstack__react-router.mjs +1480 -0
- package/.output/server/_libs/tanstack__react-store.mjs +1 -0
- package/.output/server/_libs/tanstack__react-virtual.mjs +44 -0
- package/.output/server/_libs/tanstack__router-core.mjs +4827 -0
- package/.output/server/_libs/tanstack__store.mjs +1 -0
- package/.output/server/_libs/tanstack__virtual-core.mjs +1225 -0
- package/.output/server/_libs/tiny-invariant.mjs +12 -0
- package/.output/server/_libs/tiny-warning.mjs +5 -0
- package/.output/server/_libs/trim-lines.mjs +41 -0
- package/.output/server/_libs/trough.mjs +85 -0
- package/.output/server/_libs/tslib.mjs +1 -0
- package/.output/server/_libs/ufo.mjs +54 -0
- package/.output/server/_libs/uint8array-extras.mjs +69 -0
- package/.output/server/_libs/ungap__structured-clone.mjs +212 -0
- package/.output/server/_libs/unified.mjs +661 -0
- package/.output/server/_libs/unist-util-is.mjs +100 -0
- package/.output/server/_libs/unist-util-position.mjs +27 -0
- package/.output/server/_libs/unist-util-stringify-position.mjs +27 -0
- package/.output/server/_libs/unist-util-visit-parents.mjs +82 -0
- package/.output/server/_libs/unist-util-visit.mjs +24 -0
- package/.output/server/_libs/unstorage.mjs +1 -0
- package/.output/server/_libs/use-callback-ref.mjs +66 -0
- package/.output/server/_libs/use-sidecar.mjs +106 -0
- package/.output/server/_libs/use-sync-external-store.mjs +64 -0
- package/.output/server/_libs/util-deprecate.mjs +12 -0
- package/.output/server/_libs/vfile-message.mjs +138 -0
- package/.output/server/_libs/vfile.mjs +467 -0
- package/.output/server/_libs/when-exit.mjs +53 -0
- package/.output/server/_libs/zod.mjs +4524 -0
- package/.output/server/_sessionId-wMLPvC5g.mjs +123 -0
- package/.output/server/_ssr/CompareDrawer-BU4V0uVf.mjs +1041 -0
- package/.output/server/_ssr/ProxyViewerContainer-BnRwFEnn.mjs +5972 -0
- package/.output/server/_ssr/ReplayDialog-C7dn9pd_.mjs +322 -0
- package/.output/server/_ssr/RequestAnatomy-C1rWpe9-.mjs +353 -0
- package/.output/server/_ssr/ResponseView-hGpPaYsf.mjs +602 -0
- package/.output/server/_ssr/StreamingChunkSequence-BRWI1r_G.mjs +302 -0
- package/.output/server/_ssr/index-BKURLVPz.mjs +118 -0
- package/.output/server/_ssr/index.mjs +1184 -0
- package/.output/server/_ssr/json-viewer-BBd2DtQP.mjs +515 -0
- package/.output/server/_ssr/router-BcZ0D6AB.mjs +6317 -0
- package/.output/server/_ssr/start-HYkvq4Ni.mjs +4 -0
- package/.output/server/_tanstack-start-manifest_v-1y8ZVxRI.mjs +4 -0
- package/.output/server/index.mjs +436 -0
- package/.output/server/node_modules/tslib/modules/index.js +70 -0
- package/.output/server/node_modules/tslib/modules/package.json +3 -0
- package/.output/server/node_modules/tslib/package.json +47 -0
- package/.output/server/node_modules/tslib/tslib.js +484 -0
- package/.output/server/package.json +9 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/package.json +110 -0
- package/src/assets/favicon.svg +31 -0
- package/src/assets/logos/alibaba.svg +1 -0
- package/src/assets/logos/anthropic.svg +1 -0
- package/src/assets/logos/claude-code.svg +4 -0
- package/src/assets/logos/deepseek.svg +1 -0
- package/src/assets/logos/mcp.png +0 -0
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +1 -0
- package/src/assets/logos/opencode.svg +4 -0
- package/src/assets/logos/qwen.png +0 -0
- package/src/assets/logos/zhipuai.svg +219 -0
- package/src/cli/detect-tools.ts +147 -0
- package/src/cli/doctor.ts +521 -0
- package/src/cli/onboard.ts +224 -0
- package/src/cli/templates/command-onboard.ts +17 -0
- package/src/cli/templates/skill-onboard.ts +547 -0
- package/src/cli.ts +345 -0
- package/src/components/OnboardingBanner.tsx +67 -0
- package/src/components/ProxyViewer.tsx +545 -0
- package/src/components/ProxyViewerContainer.tsx +363 -0
- package/src/components/providers/ImportWizardDialog.tsx +349 -0
- package/src/components/providers/ProviderCard.tsx +474 -0
- package/src/components/providers/ProviderForm.tsx +494 -0
- package/src/components/providers/ProviderLogo.tsx +117 -0
- package/src/components/providers/ProvidersPanel.tsx +619 -0
- package/src/components/providers/SettingsDialog.tsx +202 -0
- package/src/components/proxy-viewer/CompareDrawer.tsx +893 -0
- package/src/components/proxy-viewer/ConversationGroup.tsx +107 -0
- package/src/components/proxy-viewer/ConversationHeader.tsx +300 -0
- package/src/components/proxy-viewer/LogEntry.tsx +543 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +501 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +218 -0
- package/src/components/proxy-viewer/ResponseView.tsx +171 -0
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +188 -0
- package/src/components/proxy-viewer/ThreadConnector.tsx +136 -0
- package/src/components/proxy-viewer/TurnGroup.tsx +337 -0
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +98 -0
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +196 -0
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +53 -0
- package/src/components/proxy-viewer/anatomy/types.ts +39 -0
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +114 -0
- package/src/components/proxy-viewer/cacheTrend.ts +50 -0
- package/src/components/proxy-viewer/diff/DiffView.tsx +321 -0
- package/src/components/proxy-viewer/diff/computeDiff.ts +178 -0
- package/src/components/proxy-viewer/diff/index.ts +3 -0
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +157 -0
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +66 -0
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
- package/src/components/proxy-viewer/formats/index.tsx +33 -0
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +170 -0
- package/src/components/proxy-viewer/index.ts +9 -0
- package/src/components/proxy-viewer/lazy.ts +37 -0
- package/src/components/proxy-viewer/log-formats/anthropic.ts +194 -0
- package/src/components/proxy-viewer/log-formats/index.ts +23 -0
- package/src/components/proxy-viewer/log-formats/openai.ts +167 -0
- package/src/components/proxy-viewer/log-formats/types.ts +40 -0
- package/src/components/proxy-viewer/log-formats/unknown.ts +18 -0
- package/src/components/proxy-viewer/logEntryVisibility.ts +39 -0
- package/src/components/proxy-viewer/requestDiff.ts +277 -0
- package/src/components/proxy-viewer/useCopyFeedback.ts +36 -0
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +190 -0
- package/src/components/proxy-viewer/viewerState.ts +66 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/button.tsx +47 -0
- package/src/components/ui/collapsible.tsx +21 -0
- package/src/components/ui/confirm-dialog.tsx +51 -0
- package/src/components/ui/crab-logo.tsx +95 -0
- package/src/components/ui/crab-variants.tsx +467 -0
- package/src/components/ui/dialog.tsx +129 -0
- package/src/components/ui/json-expansion-button.tsx +56 -0
- package/src/components/ui/json-viewer-bulk.ts +97 -0
- package/src/components/ui/json-viewer.tsx +494 -0
- package/src/components/ui/mcp-logo.tsx +20 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/select.tsx +178 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +88 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/index.css +11 -0
- package/src/knowledge/candidateStore.ts +63 -0
- package/src/knowledge/distiller.ts +98 -0
- package/src/knowledge/openclawClient.ts +118 -0
- package/src/knowledge/redactor.ts +80 -0
- package/src/knowledge/types.ts +84 -0
- package/src/lib/apiClient.ts +49 -0
- package/src/lib/export-logs.ts +51 -0
- package/src/lib/mask.ts +4 -0
- package/src/lib/objectUtils.ts +22 -0
- package/src/lib/providerContract.ts +26 -0
- package/src/lib/providerTestContract.ts +107 -0
- package/src/lib/runtimeConfig.ts +25 -0
- package/src/lib/serverPort.ts +41 -0
- package/src/lib/sessionUrl.ts +44 -0
- package/src/lib/stopReason.ts +58 -0
- package/src/lib/useOnboarding.ts +80 -0
- package/src/lib/useProviders.ts +30 -0
- package/src/lib/useStripConfig.ts +108 -0
- package/src/lib/utils.ts +21 -0
- package/src/mcp/loopback.ts +76 -0
- package/src/mcp/previewExtractor.ts +166 -0
- package/src/mcp/server.ts +396 -0
- package/src/mcp/toolHandlers.ts +341 -0
- package/src/proxy/chunkStorage.ts +112 -0
- package/src/proxy/claudeCodeStrip.ts +99 -0
- package/src/proxy/config.ts +172 -0
- package/src/proxy/constants.ts +47 -0
- package/src/proxy/dataDir.ts +86 -0
- package/src/proxy/formats/anthropic/anthropicProvider.ts +75 -0
- package/src/proxy/formats/anthropic/handler.ts +71 -0
- package/src/proxy/formats/anthropic/index.ts +14 -0
- package/src/proxy/formats/anthropic/register.ts +4 -0
- package/src/proxy/formats/anthropic/schemas.ts +237 -0
- package/src/proxy/formats/anthropic/stream.ts +205 -0
- package/src/proxy/formats/handler.ts +46 -0
- package/src/proxy/formats/index.ts +12 -0
- package/src/proxy/formats/jsonSchema.ts +36 -0
- package/src/proxy/formats/openai/alibabaProvider.ts +38 -0
- package/src/proxy/formats/openai/handler.ts +96 -0
- package/src/proxy/formats/openai/index.ts +25 -0
- package/src/proxy/formats/openai/provider.ts +50 -0
- package/src/proxy/formats/openai/register.ts +4 -0
- package/src/proxy/formats/openai/schemas.ts +187 -0
- package/src/proxy/formats/openai/stream.ts +206 -0
- package/src/proxy/formats/protocol.ts +50 -0
- package/src/proxy/formats/providerRegistry.ts +51 -0
- package/src/proxy/formats/providers/index.ts +3 -0
- package/src/proxy/formats/registry.ts +66 -0
- package/src/proxy/handler.ts +334 -0
- package/src/proxy/logFinalizer.ts +305 -0
- package/src/proxy/logFinalizer.worker.ts +24 -0
- package/src/proxy/logIndex.ts +268 -0
- package/src/proxy/logger.ts +179 -0
- package/src/proxy/openaiOrphanToolStrip.ts +142 -0
- package/src/proxy/providerImporters.ts +491 -0
- package/src/proxy/providers.ts +613 -0
- package/src/proxy/schemas.ts +209 -0
- package/src/proxy/sessionProcess.ts +140 -0
- package/src/proxy/sessionRuntime.ts +85 -0
- package/src/proxy/sessionSupervisor.ts +283 -0
- package/src/proxy/sessionWorkerEntry.ts +26 -0
- package/src/proxy/socketTracker.ts +255 -0
- package/src/proxy/store.ts +412 -0
- package/src/proxy/upstream.ts +90 -0
- package/src/router.tsx +16 -0
- package/src/routes/__root.tsx +45 -0
- package/src/routes/api/config.paths.ts +14 -0
- package/src/routes/api/config.ts +53 -0
- package/src/routes/api/health.ts +15 -0
- package/src/routes/api/knowledge.candidates.$candidateId.promote.ts +32 -0
- package/src/routes/api/knowledge.candidates.ts +10 -0
- package/src/routes/api/knowledge.project-context.ts +18 -0
- package/src/routes/api/knowledge.search.ts +31 -0
- package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +16 -0
- package/src/routes/api/logs.$id.chunks.ts +36 -0
- package/src/routes/api/logs.$id.replay.ts +191 -0
- package/src/routes/api/logs.$id.ts +22 -0
- package/src/routes/api/logs.stream.ts +74 -0
- package/src/routes/api/logs.ts +59 -0
- package/src/routes/api/mcp.ts +25 -0
- package/src/routes/api/models.ts +10 -0
- package/src/routes/api/providers.$providerId.test.log.ts +293 -0
- package/src/routes/api/providers.$providerId.ts +50 -0
- package/src/routes/api/providers.export.ts +26 -0
- package/src/routes/api/providers.import.ts +47 -0
- package/src/routes/api/providers.scan.ts +23 -0
- package/src/routes/api/providers.ts +45 -0
- package/src/routes/api/sessions.ts +17 -0
- package/src/routes/index.tsx +6 -0
- package/src/routes/proxy/$.ts +15 -0
- package/src/routes/session/$sessionId.tsx +23 -0
- package/styles/globals.css +188 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { createLog, finalizeLogUpdate, getNextLogId, type CapturedLog } from "./store";
|
|
2
|
+
import { appendLogEntry, logger } from "./logger";
|
|
3
|
+
import { extractRequestMetadata } from "./schemas";
|
|
4
|
+
import { registry } from "./formats";
|
|
5
|
+
import { findProviderByModel } from "./providers";
|
|
6
|
+
import { getClientInfo } from "./socketTracker";
|
|
7
|
+
import { formatForPath } from "./formats";
|
|
8
|
+
import {
|
|
9
|
+
PROXY_IDENTITY,
|
|
10
|
+
PRESERVE_HEADERS,
|
|
11
|
+
PATH_V1_MESSAGES,
|
|
12
|
+
HEADER_CONTENT_TYPE,
|
|
13
|
+
HEADER_USER_AGENT,
|
|
14
|
+
HEADER_X_PROXY_IDENTITY,
|
|
15
|
+
HEADER_CONTENT_ENCODING,
|
|
16
|
+
HEADER_CONTENT_LENGTH,
|
|
17
|
+
CONTENT_TYPE_EVENT_STREAM,
|
|
18
|
+
STATUS_FORBIDDEN,
|
|
19
|
+
STATUS_BAD_GATEWAY,
|
|
20
|
+
} from "./constants";
|
|
21
|
+
import { getConfig } from "./config";
|
|
22
|
+
import { stripClaudeCodeBillingHeader } from "./claudeCodeStrip";
|
|
23
|
+
import { stripOpenAIOrphanToolMessages } from "./openaiOrphanToolStrip";
|
|
24
|
+
import { buildFileLogEntry, type FinalizeLogJob } from "./logFinalizer";
|
|
25
|
+
import { enqueueFinalizeLogJob } from "./sessionRuntime";
|
|
26
|
+
import {
|
|
27
|
+
buildUpstreamUrl,
|
|
28
|
+
describeApiRoute,
|
|
29
|
+
getProxyApiPath,
|
|
30
|
+
injectProviderAuth,
|
|
31
|
+
selectUpstreamBase,
|
|
32
|
+
setUpstreamHost,
|
|
33
|
+
} from "./upstream";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Strips all custom/non-standard headers from the request and replaces with
|
|
37
|
+
* unified proxy identity. Only preserves standard HTTP headers needed for API calls.
|
|
38
|
+
*/
|
|
39
|
+
function buildProxyHeaders(originalHeaders: Headers): {
|
|
40
|
+
headers: Headers;
|
|
41
|
+
rawHeaders: Record<string, string>;
|
|
42
|
+
} {
|
|
43
|
+
const rawHeaders: Record<string, string> = {};
|
|
44
|
+
const headers = new Headers();
|
|
45
|
+
|
|
46
|
+
originalHeaders.forEach((value, key) => {
|
|
47
|
+
rawHeaders[key.toLowerCase()] = value;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
headers.set(HEADER_USER_AGENT, PROXY_IDENTITY);
|
|
51
|
+
headers.set(HEADER_X_PROXY_IDENTITY, PROXY_IDENTITY);
|
|
52
|
+
|
|
53
|
+
for (const name of PRESERVE_HEADERS) {
|
|
54
|
+
const value = originalHeaders.get(name);
|
|
55
|
+
if (value !== null) {
|
|
56
|
+
headers.set(name, value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { headers, rawHeaders };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type ParsedRequestPath = {
|
|
64
|
+
apiPath: string;
|
|
65
|
+
isMessages: boolean;
|
|
66
|
+
isChatCompletions: boolean;
|
|
67
|
+
normalizedPath: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function parseRequestPath(req: Request, url: URL): ParsedRequestPath {
|
|
71
|
+
const route = describeApiRoute(getProxyApiPath(url));
|
|
72
|
+
const isPost = req.method === "POST";
|
|
73
|
+
const isChatCompletions = isPost && route.isChatCompletions;
|
|
74
|
+
const isMessages = isPost && (route.endpointPath === PATH_V1_MESSAGES || route.isChatCompletions);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
apiPath: route.apiPath,
|
|
78
|
+
isMessages,
|
|
79
|
+
isChatCompletions,
|
|
80
|
+
normalizedPath: route.normalizedPath,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function errorMessage(err: unknown): string {
|
|
85
|
+
return err instanceof Error ? err.message : String(err);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scheduleLogFinalization(job: FinalizeLogJob): void {
|
|
89
|
+
void enqueueFinalizeLogJob(job).catch((err) => {
|
|
90
|
+
logger.error(
|
|
91
|
+
`[handler] Session finalization task failed for log #${job.log.id}:`,
|
|
92
|
+
errorMessage(err),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleNonStreamingResponse(
|
|
98
|
+
upstreamRes: Response,
|
|
99
|
+
responseBody: string,
|
|
100
|
+
startTime: number,
|
|
101
|
+
upstreamUrl: string,
|
|
102
|
+
log: CapturedLog,
|
|
103
|
+
): Response {
|
|
104
|
+
const elapsedMs = Date.now() - startTime;
|
|
105
|
+
|
|
106
|
+
scheduleLogFinalization({
|
|
107
|
+
type: "non-streaming",
|
|
108
|
+
log,
|
|
109
|
+
upstreamUrl,
|
|
110
|
+
elapsedMs,
|
|
111
|
+
responseStatus: upstreamRes.status,
|
|
112
|
+
responseBody,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const responseHeaders = new Headers(upstreamRes.headers);
|
|
116
|
+
responseHeaders.delete(HEADER_CONTENT_ENCODING);
|
|
117
|
+
responseHeaders.delete(HEADER_CONTENT_LENGTH);
|
|
118
|
+
|
|
119
|
+
return new Response(responseBody, {
|
|
120
|
+
status: upstreamRes.status,
|
|
121
|
+
headers: responseHeaders,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleStreamingResponse(
|
|
126
|
+
upstreamRes: Response,
|
|
127
|
+
req: Request,
|
|
128
|
+
startTime: number,
|
|
129
|
+
upstreamUrl: string,
|
|
130
|
+
log: CapturedLog,
|
|
131
|
+
): Response {
|
|
132
|
+
log.streaming = true;
|
|
133
|
+
log.responseStatus = upstreamRes.status;
|
|
134
|
+
|
|
135
|
+
const chunks: string[] = [];
|
|
136
|
+
const decoder = new TextDecoder();
|
|
137
|
+
|
|
138
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
139
|
+
transform(chunk, controller) {
|
|
140
|
+
controller.enqueue(chunk);
|
|
141
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
142
|
+
},
|
|
143
|
+
flush() {
|
|
144
|
+
const full = chunks.join("");
|
|
145
|
+
const elapsedMs = Date.now() - startTime;
|
|
146
|
+
|
|
147
|
+
scheduleLogFinalization({
|
|
148
|
+
type: "streaming",
|
|
149
|
+
log,
|
|
150
|
+
upstreamUrl,
|
|
151
|
+
elapsedMs,
|
|
152
|
+
responseStatus: upstreamRes.status,
|
|
153
|
+
rawStream: full,
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (upstreamRes.body === null) {
|
|
159
|
+
return new Response("No response body", { status: STATUS_BAD_GATEWAY });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const loggedStream = upstreamRes.body.pipeThrough(transform);
|
|
163
|
+
|
|
164
|
+
req.signal?.addEventListener("abort", () => {
|
|
165
|
+
if (log.responseText === null) {
|
|
166
|
+
logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
|
|
167
|
+
const elapsedMs = Date.now() - startTime;
|
|
168
|
+
const full = chunks.join("");
|
|
169
|
+
const hasChunks = chunks.length > 0;
|
|
170
|
+
|
|
171
|
+
scheduleLogFinalization({
|
|
172
|
+
type: "stream-abort",
|
|
173
|
+
log,
|
|
174
|
+
upstreamUrl,
|
|
175
|
+
elapsedMs,
|
|
176
|
+
rawStream: full,
|
|
177
|
+
hasChunks,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const responseHeaders = new Headers(upstreamRes.headers);
|
|
183
|
+
responseHeaders.delete(HEADER_CONTENT_ENCODING);
|
|
184
|
+
responseHeaders.delete(HEADER_CONTENT_LENGTH);
|
|
185
|
+
|
|
186
|
+
return new Response(loggedStream, {
|
|
187
|
+
status: upstreamRes.status,
|
|
188
|
+
headers: responseHeaders,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function handleProxy(req: Request): Promise<Response> {
|
|
193
|
+
const url = new URL(req.url);
|
|
194
|
+
const parsed = parseRequestPath(req, url);
|
|
195
|
+
|
|
196
|
+
// Read body only after cheap path checks
|
|
197
|
+
let requestBody: string | null = null;
|
|
198
|
+
if (req.body && req.method !== "GET" && req.method !== "HEAD") {
|
|
199
|
+
requestBody = await req.text();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Opt-in: strip Claude Code's synthetic billing-header text block from the
|
|
203
|
+
// system prompt. Only runs for Anthropic-format requests (those whose path
|
|
204
|
+
// resolves to /v1/messages). The flag is read once per request from the
|
|
205
|
+
// runtime config, so a PATCH to /api/config takes effect on the next
|
|
206
|
+
// request without a restart.
|
|
207
|
+
//
|
|
208
|
+
// Important: the original `requestBody` must be preserved so the log stores
|
|
209
|
+
// the unmodified request (the "Raw Request" tab needs the true bytes the
|
|
210
|
+
// client sent). The rewritten body is only used for the upstream fetch.
|
|
211
|
+
let bodyToForward = requestBody;
|
|
212
|
+
if (getConfig().stripClaudeCodeBillingHeader && requestBody !== null && parsed.isMessages) {
|
|
213
|
+
const stripped = stripClaudeCodeBillingHeader(requestBody);
|
|
214
|
+
if (stripped.removed > 0) {
|
|
215
|
+
logger.info(
|
|
216
|
+
`[handler] Stripped ${stripped.removed} Claude Code billing-header block(s) from system prompt`,
|
|
217
|
+
);
|
|
218
|
+
bodyToForward = stripped.body;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Always-on protocol guard: OpenAI Chat Completions requires every
|
|
223
|
+
// `role: "tool"` message to reference a `tool_call_id` present in a
|
|
224
|
+
// preceding assistant message's `tool_calls`. Buggy clients sometimes
|
|
225
|
+
// record a tool result without the matching assistant tool_call, which
|
|
226
|
+
// makes the upstream reject the request with a 400 ("tool result's tool
|
|
227
|
+
// id(call_xxx) not found"). Drop the orphan messages so the proxy can
|
|
228
|
+
// forward a valid request; the original body is still kept in the log.
|
|
229
|
+
if (bodyToForward !== null && parsed.isChatCompletions) {
|
|
230
|
+
const stripped = stripOpenAIOrphanToolMessages(bodyToForward);
|
|
231
|
+
if (stripped.removed > 0) {
|
|
232
|
+
logger.warn(
|
|
233
|
+
`[handler] Dropped ${stripped.removed} orphan OpenAI tool message(s) with tool_call_id(s) ${JSON.stringify(stripped.orphanIds)} — the client sent a tool result with no matching assistant.tool_calls`,
|
|
234
|
+
);
|
|
235
|
+
bodyToForward = stripped.body;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Parse the request body exactly once. The handler needs the model for
|
|
240
|
+
// provider routing and the session id for the log; `createLog` would
|
|
241
|
+
// otherwise re-parse the same body to extract the same fields.
|
|
242
|
+
const { model, sessionId } = extractRequestMetadata(requestBody, req.headers);
|
|
243
|
+
|
|
244
|
+
// Find provider config using already-extracted model (not calling extractModelFromBody again)
|
|
245
|
+
const matchedProviderConfig = model !== null ? findProviderByModel(model) : null;
|
|
246
|
+
const route = describeApiRoute(parsed.apiPath);
|
|
247
|
+
const upstreamBase = selectUpstreamBase(route, matchedProviderConfig);
|
|
248
|
+
const upstreamUrl = buildUpstreamUrl(upstreamBase, parsed.normalizedPath);
|
|
249
|
+
const startTime = Date.now();
|
|
250
|
+
|
|
251
|
+
const { headers: upstreamHeaders, rawHeaders } = buildProxyHeaders(req.headers);
|
|
252
|
+
setUpstreamHost(upstreamHeaders, upstreamBase);
|
|
253
|
+
injectProviderAuth(upstreamHeaders, matchedProviderConfig);
|
|
254
|
+
|
|
255
|
+
const provider = model !== null ? registry.findProvider(model) : null;
|
|
256
|
+
|
|
257
|
+
// Only proxy requests matching a registered provider
|
|
258
|
+
if (model === null || provider === null) {
|
|
259
|
+
logger.warn(`[handler] Unsupported provider: model=${model}`);
|
|
260
|
+
return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Pick the format handler from the request path. provider.format is only a
|
|
264
|
+
// hint used as a fallback when no anthropicBaseUrl/openaiBaseUrl is set, and
|
|
265
|
+
// does NOT describe what the upstream actually returns: a provider configured
|
|
266
|
+
// with format="openai" may still expose an Anthropic-compatible endpoint
|
|
267
|
+
// (e.g. MiniMax's /anthropic path), and the response shape always follows
|
|
268
|
+
// the URL path the client hit, not the provider.format label.
|
|
269
|
+
const formatHandler = formatForPath(parsed.apiPath);
|
|
270
|
+
if (formatHandler === null) {
|
|
271
|
+
return new Response("Forbidden: unsupported format", { status: STATUS_FORBIDDEN });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Acquire client info and the next log id in parallel. On Windows
|
|
275
|
+
// `getClientInfo` shells out to PowerShell and is the single slowest
|
|
276
|
+
// thing on the hot path; serializing it before `createLog` paid the
|
|
277
|
+
// full PowerShell round-trip on top of everything else before
|
|
278
|
+
// `fetch` is even issued.
|
|
279
|
+
const [clientInfo, preAcquiredId] = await Promise.all([getClientInfo(req), getNextLogId()]);
|
|
280
|
+
const upstreamHeadersObj: Record<string, string> = {};
|
|
281
|
+
upstreamHeaders.forEach((value, key) => {
|
|
282
|
+
upstreamHeadersObj[key.toLowerCase()] = value;
|
|
283
|
+
});
|
|
284
|
+
const log = await createLog(
|
|
285
|
+
req.method,
|
|
286
|
+
parsed.apiPath,
|
|
287
|
+
requestBody,
|
|
288
|
+
req.headers,
|
|
289
|
+
clientInfo,
|
|
290
|
+
rawHeaders,
|
|
291
|
+
upstreamHeadersObj,
|
|
292
|
+
formatHandler.format,
|
|
293
|
+
model,
|
|
294
|
+
sessionId,
|
|
295
|
+
preAcquiredId,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
let upstreamRes: Response;
|
|
299
|
+
try {
|
|
300
|
+
upstreamRes = await fetch(upstreamUrl, {
|
|
301
|
+
method: req.method,
|
|
302
|
+
headers: upstreamHeaders,
|
|
303
|
+
body: bodyToForward,
|
|
304
|
+
signal: req.signal,
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
log.elapsedMs = Date.now() - startTime;
|
|
308
|
+
// Check if it was a client abort (not a proxy error)
|
|
309
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
310
|
+
logger.info(`[handler] Client aborted: ${req.method} ${parsed.apiPath}`);
|
|
311
|
+
log.responseStatus = 499; // Client Closed Request (non-standard but descriptive)
|
|
312
|
+
log.responseText = "Client aborted";
|
|
313
|
+
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
|
|
314
|
+
finalizeLogUpdate(log);
|
|
315
|
+
return new Response("Client aborted", { status: 499 });
|
|
316
|
+
}
|
|
317
|
+
logger.error(`[handler] Proxy error: ${req.method} ${parsed.apiPath}`, String(err));
|
|
318
|
+
log.responseStatus = STATUS_BAD_GATEWAY;
|
|
319
|
+
log.responseText = String(err);
|
|
320
|
+
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: String(err) });
|
|
321
|
+
finalizeLogUpdate(log);
|
|
322
|
+
return new Response(`Proxy error: ${err}`, { status: STATUS_BAD_GATEWAY });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const isStream =
|
|
326
|
+
upstreamRes.headers.get(HEADER_CONTENT_TYPE)?.includes(CONTENT_TYPE_EVENT_STREAM) ?? false;
|
|
327
|
+
|
|
328
|
+
if (!isStream) {
|
|
329
|
+
const responseBody = await upstreamRes.text();
|
|
330
|
+
return handleNonStreamingResponse(upstreamRes, responseBody, startTime, upstreamUrl, log);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return handleStreamingResponse(upstreamRes, req, startTime, upstreamUrl, log);
|
|
334
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { writeChunks } from "./chunkStorage";
|
|
3
|
+
import { formatForPath } from "./formats";
|
|
4
|
+
import { appendLogEntry, logger } from "./logger";
|
|
5
|
+
import type { CapturedLog } from "./schemas";
|
|
6
|
+
import { getSessionProcess, isSessionProcessAvailable } from "./sessionProcess";
|
|
7
|
+
import { finalizeLogUpdate } from "./store";
|
|
8
|
+
|
|
9
|
+
type BaseFinalizeLogJob = {
|
|
10
|
+
log: CapturedLog;
|
|
11
|
+
upstreamUrl: string;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type FinalizeNonStreamingLogJob = BaseFinalizeLogJob & {
|
|
16
|
+
type: "non-streaming";
|
|
17
|
+
responseStatus: number;
|
|
18
|
+
responseBody: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type FinalizeStreamingLogJob = BaseFinalizeLogJob & {
|
|
22
|
+
type: "streaming";
|
|
23
|
+
responseStatus: number;
|
|
24
|
+
rawStream: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
|
|
28
|
+
type: "stream-abort";
|
|
29
|
+
rawStream: string;
|
|
30
|
+
hasChunks: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type FinalizeLogJob =
|
|
34
|
+
| FinalizeNonStreamingLogJob
|
|
35
|
+
| FinalizeStreamingLogJob
|
|
36
|
+
| FinalizeStreamAbortLogJob;
|
|
37
|
+
|
|
38
|
+
export type FinalizeLogResult = {
|
|
39
|
+
log: CapturedLog;
|
|
40
|
+
upstreamUrl: string;
|
|
41
|
+
error: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record<string, unknown> {
|
|
45
|
+
return {
|
|
46
|
+
timestamp: log.timestamp,
|
|
47
|
+
id: log.id,
|
|
48
|
+
method: log.method,
|
|
49
|
+
path: log.path,
|
|
50
|
+
model: log.model,
|
|
51
|
+
sessionId: log.sessionId,
|
|
52
|
+
rawRequestBody: log.rawRequestBody,
|
|
53
|
+
responseStatus: log.responseStatus,
|
|
54
|
+
responseText: log.responseText,
|
|
55
|
+
inputTokens: log.inputTokens,
|
|
56
|
+
outputTokens: log.outputTokens,
|
|
57
|
+
cacheCreationInputTokens: log.cacheCreationInputTokens,
|
|
58
|
+
cacheReadInputTokens: log.cacheReadInputTokens,
|
|
59
|
+
elapsedMs: log.elapsedMs,
|
|
60
|
+
streaming: log.streaming,
|
|
61
|
+
userAgent: log.userAgent,
|
|
62
|
+
origin: log.origin,
|
|
63
|
+
upstreamUrl,
|
|
64
|
+
clientPort: log.clientPort,
|
|
65
|
+
clientPid: log.clientPid,
|
|
66
|
+
clientCwd: log.clientCwd,
|
|
67
|
+
clientProjectFolder: log.clientProjectFolder,
|
|
68
|
+
streamingChunks: log.streamingChunks,
|
|
69
|
+
streamingChunksPath: log.streamingChunksPath,
|
|
70
|
+
error: log.error,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cloneLog(log: CapturedLog): CapturedLog {
|
|
75
|
+
return { ...log };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function errorMessage(err: unknown): string {
|
|
79
|
+
return err instanceof Error ? err.message : String(err);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function persistStreamingChunks(log: CapturedLog): void {
|
|
83
|
+
if (log.streamingChunks !== undefined && log.streamingChunks.chunks.length > 0) {
|
|
84
|
+
const chunkPath = writeChunks(
|
|
85
|
+
log.id,
|
|
86
|
+
log.streamingChunks.chunks,
|
|
87
|
+
log.streamingChunks.truncated,
|
|
88
|
+
);
|
|
89
|
+
log.streamingChunksPath = chunkPath;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function finalizeWithError(
|
|
94
|
+
job: FinalizeLogJob,
|
|
95
|
+
log: CapturedLog,
|
|
96
|
+
fallbackStatus: number,
|
|
97
|
+
fallbackResponseText: string,
|
|
98
|
+
err: unknown,
|
|
99
|
+
): FinalizeLogResult {
|
|
100
|
+
const message = errorMessage(err);
|
|
101
|
+
logger.error(`[logFinalizer] Failed to finalize log #${log.id}:`, message);
|
|
102
|
+
log.responseStatus = log.responseStatus ?? fallbackStatus;
|
|
103
|
+
log.responseText = log.responseText ?? fallbackResponseText;
|
|
104
|
+
log.elapsedMs = job.elapsedMs;
|
|
105
|
+
log.error = message;
|
|
106
|
+
return { log, upstreamUrl: job.upstreamUrl, error: message };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function finalizeNonStreaming(
|
|
110
|
+
job: FinalizeNonStreamingLogJob,
|
|
111
|
+
log: CapturedLog,
|
|
112
|
+
): FinalizeLogResult {
|
|
113
|
+
const formatHandler = formatForPath(log.path);
|
|
114
|
+
if (formatHandler === null) {
|
|
115
|
+
return finalizeWithError(job, log, job.responseStatus, job.responseBody, "Unsupported format");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const tokens = formatHandler.extractTokens(job.responseBody);
|
|
120
|
+
|
|
121
|
+
log.elapsedMs = job.elapsedMs;
|
|
122
|
+
log.responseStatus = job.responseStatus;
|
|
123
|
+
log.responseText = job.responseBody;
|
|
124
|
+
log.inputTokens = tokens.inputTokens;
|
|
125
|
+
log.outputTokens = tokens.outputTokens;
|
|
126
|
+
log.cacheCreationInputTokens = tokens.cacheCreationInputTokens;
|
|
127
|
+
log.cacheReadInputTokens = tokens.cacheReadInputTokens;
|
|
128
|
+
|
|
129
|
+
return { log, upstreamUrl: job.upstreamUrl, error: null };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return finalizeWithError(job, log, job.responseStatus, job.responseBody, err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): FinalizeLogResult {
|
|
136
|
+
const formatHandler = formatForPath(log.path);
|
|
137
|
+
if (formatHandler === null) {
|
|
138
|
+
return finalizeWithError(job, log, job.responseStatus, job.rawStream, "Unsupported format");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
log.elapsedMs = job.elapsedMs;
|
|
143
|
+
log.responseStatus = job.responseStatus;
|
|
144
|
+
log.responseText = formatHandler.extractStream(
|
|
145
|
+
job.rawStream,
|
|
146
|
+
log,
|
|
147
|
+
log.model ?? undefined,
|
|
148
|
+
true,
|
|
149
|
+
);
|
|
150
|
+
persistStreamingChunks(log);
|
|
151
|
+
|
|
152
|
+
return { log, upstreamUrl: job.upstreamUrl, error: null };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return finalizeWithError(job, log, job.responseStatus, job.rawStream, err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog): FinalizeLogResult {
|
|
159
|
+
const formatHandler = formatForPath(log.path);
|
|
160
|
+
if (formatHandler === null && job.hasChunks) {
|
|
161
|
+
return finalizeWithError(job, log, 499, "Client aborted", "Unsupported format");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
log.elapsedMs = job.elapsedMs;
|
|
166
|
+
if (job.hasChunks && formatHandler !== null) {
|
|
167
|
+
log.responseText = formatHandler.extractStream(
|
|
168
|
+
job.rawStream,
|
|
169
|
+
log,
|
|
170
|
+
log.model ?? undefined,
|
|
171
|
+
true,
|
|
172
|
+
);
|
|
173
|
+
persistStreamingChunks(log);
|
|
174
|
+
} else {
|
|
175
|
+
log.responseText = "Client aborted";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { log, upstreamUrl: job.upstreamUrl, error: "Client aborted" };
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return finalizeWithError(job, log, 499, "Client aborted", err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function buildFinalizeLogResult(job: FinalizeLogJob): FinalizeLogResult {
|
|
185
|
+
const log = cloneLog(job.log);
|
|
186
|
+
|
|
187
|
+
switch (job.type) {
|
|
188
|
+
case "non-streaming":
|
|
189
|
+
return finalizeNonStreaming(job, log);
|
|
190
|
+
case "streaming":
|
|
191
|
+
return finalizeStreaming(job, log);
|
|
192
|
+
case "stream-abort":
|
|
193
|
+
return finalizeStreamAbort(job, log);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function commitFinalizeLogResult(result: FinalizeLogResult): void {
|
|
198
|
+
appendLogEntry({ ...buildFileLogEntry(result.log, result.upstreamUrl), error: result.error });
|
|
199
|
+
finalizeLogUpdate(result.log);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Worker pool ────────────────────────────────────────────────
|
|
203
|
+
// buildFinalizeLogResult is the CPU-heavy step (token extraction,
|
|
204
|
+
// SSE parsing, chunk persistence). We offload it to a fixed pool
|
|
205
|
+
// of Worker Threads so the main thread stays responsive even under
|
|
206
|
+
// concurrent sessions. commitFinalizeLogResult stays in-process
|
|
207
|
+
// because it touches the in-memory cache, SSE emitter, and
|
|
208
|
+
// append‑only log — all of which belong to the main thread.
|
|
209
|
+
|
|
210
|
+
// ── Routing ─────────────────────────────────────────────────────
|
|
211
|
+
// FINALIZER_RUNTIME selects the execution backend:
|
|
212
|
+
// "process" → per-session child process (max isolation)
|
|
213
|
+
// "worker" → shared Worker Thread pool
|
|
214
|
+
// "inline" → synchronous in-process (debug / fallback)
|
|
215
|
+
// The default is "process" only when its worker entry exists. Production
|
|
216
|
+
// bundles that do not emit the standalone worker entry fall back to "inline"
|
|
217
|
+
// instead of repeatedly spawning a failing child process.
|
|
218
|
+
// For backward compatibility, FINALIZER_USE_WORKER=0 forces "inline".
|
|
219
|
+
|
|
220
|
+
const RUNTIME: "process" | "worker" | "inline" = (() => {
|
|
221
|
+
if (process.env["FINALIZER_USE_WORKER"] === "0") return "inline";
|
|
222
|
+
const mode = process.env["FINALIZER_RUNTIME"];
|
|
223
|
+
if (mode === "process") return "process";
|
|
224
|
+
if (mode === "worker") return "worker";
|
|
225
|
+
if (mode === "inline") return "inline";
|
|
226
|
+
return isSessionProcessAvailable() ? "process" : "inline";
|
|
227
|
+
})();
|
|
228
|
+
|
|
229
|
+
function executeBuildInSessionProcess(job: FinalizeLogJob): Promise<FinalizeLogResult> {
|
|
230
|
+
const sessionId = job.log.sessionId ?? "__unassigned__";
|
|
231
|
+
return getSessionProcess(sessionId)
|
|
232
|
+
.enqueue(job)
|
|
233
|
+
.catch((err: unknown) => {
|
|
234
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
235
|
+
logger.error(
|
|
236
|
+
`[logFinalizer] Session process failed for log #${job.log.id}, ` +
|
|
237
|
+
`falling back to in-process: ${message}`,
|
|
238
|
+
);
|
|
239
|
+
return buildFinalizeLogResult(job);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function resolveBuildPromise(job: FinalizeLogJob): Promise<FinalizeLogResult> {
|
|
244
|
+
switch (RUNTIME) {
|
|
245
|
+
case "process":
|
|
246
|
+
return executeBuildInSessionProcess(job);
|
|
247
|
+
case "worker":
|
|
248
|
+
return executeBuildInWorker(job);
|
|
249
|
+
case "inline":
|
|
250
|
+
return Promise.resolve(buildFinalizeLogResult(job));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const WORKER_COUNT = Math.max(1, Number(process.env["FINALIZER_WORKER_COUNT"]) || 4);
|
|
255
|
+
let _workers: Worker[] | null = null;
|
|
256
|
+
const _pending = new Map<string, (result: FinalizeLogResult) => void>();
|
|
257
|
+
let _nextWorker = 0;
|
|
258
|
+
let _jobSeq = 0;
|
|
259
|
+
|
|
260
|
+
function getWorkers(): Worker[] {
|
|
261
|
+
if (_workers !== null) return _workers;
|
|
262
|
+
_workers = [];
|
|
263
|
+
for (let i = 0; i < WORKER_COUNT; i++) {
|
|
264
|
+
const w = new Worker(new URL("./logFinalizer.worker.ts", import.meta.url));
|
|
265
|
+
w.on("message", (msg: { id: string; result: FinalizeLogResult }) => {
|
|
266
|
+
const resolve = _pending.get(msg.id);
|
|
267
|
+
if (resolve !== undefined) {
|
|
268
|
+
_pending.delete(msg.id);
|
|
269
|
+
resolve(msg.result);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
w.on("error", (err) => {
|
|
273
|
+
logger.error(
|
|
274
|
+
"[logFinalizer] Worker error:",
|
|
275
|
+
err instanceof Error ? err.message : String(err),
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
_workers.push(w);
|
|
279
|
+
}
|
|
280
|
+
return _workers;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function executeBuildInWorker(job: FinalizeLogJob): Promise<FinalizeLogResult> {
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
const list = getWorkers();
|
|
286
|
+
// getWorkers() always returns at least 1 worker; the array index is safe
|
|
287
|
+
const idx = _nextWorker % list.length;
|
|
288
|
+
_nextWorker++;
|
|
289
|
+
const w = list[idx];
|
|
290
|
+
if (w === undefined) return; // unreachable, satisfies type-checker
|
|
291
|
+
const id = String(++_jobSeq);
|
|
292
|
+
_pending.set(id, resolve);
|
|
293
|
+
w.postMessage({ id, job });
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function executeFinalizeLogJob(job: FinalizeLogJob): Promise<void> {
|
|
298
|
+
return resolveBuildPromise(job).then((result) => {
|
|
299
|
+
commitFinalizeLogResult(result);
|
|
300
|
+
if (result.error !== null && result.error !== "Client aborted") {
|
|
301
|
+
return Promise.reject(new Error(result.error));
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isMainThread, parentPort } from "node:worker_threads";
|
|
2
|
+
import {
|
|
3
|
+
buildFinalizeLogResult,
|
|
4
|
+
type FinalizeLogJob,
|
|
5
|
+
type FinalizeLogResult,
|
|
6
|
+
} from "./logFinalizer";
|
|
7
|
+
|
|
8
|
+
if (!isMainThread && parentPort !== null) {
|
|
9
|
+
const port = parentPort;
|
|
10
|
+
port.on("message", (msg: { id: string; job: FinalizeLogJob }) => {
|
|
11
|
+
let result: FinalizeLogResult;
|
|
12
|
+
try {
|
|
13
|
+
result = buildFinalizeLogResult(msg.job);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
16
|
+
result = {
|
|
17
|
+
log: msg.job.log,
|
|
18
|
+
upstreamUrl: msg.job.upstreamUrl,
|
|
19
|
+
error: message,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
port.postMessage({ id: msg.id, result });
|
|
23
|
+
});
|
|
24
|
+
}
|