@tonyclaw/agent-inspector 3.1.32 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/backend/nitro.json +1 -1
- package/.output/cli.js +241 -120
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-oXUk3MBR.mjs → router-BBY_zWRK.mjs} +91 -3
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-BtfPSpkV.js → CompareDrawer-rtNElN-B.js} +1 -1
- package/.output/ui/assets/{InspectorPet-D9DPPQ3n.js → InspectorPet-QaAiuF3d.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-Buu8vkCK.js → ProxyViewerContainer-6twNZHwl.js} +4 -4
- package/.output/ui/assets/{ReplayDialog-BER-QzpD.js → ReplayDialog-B7KlziO2.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-C8Zwxrr7.js → RequestAnatomy-2XVFBrW4.js} +1 -1
- package/.output/ui/assets/{ResponseView-DDpKdYk6.js → ResponseView-BULgzLVB.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-sMPGG7Sw.js → StreamingChunkSequence-DSkvkbns.js} +1 -1
- package/.output/ui/assets/{_sessionId-e4IheHnP.js → _sessionId-D2dmJi3G.js} +1 -1
- package/.output/ui/assets/{_sessionId-C5Uv72Fx.js → _sessionId-DcS0s1Mr.js} +1 -1
- package/.output/ui/assets/{index-B__qT0Yd.js → index-B0TN1KKO.js} +2 -2
- package/.output/ui/assets/{index-Be3bTCzi.js → index-Bva1zHEs.js} +1 -1
- package/.output/ui/assets/{index-C9q__p8J.js → index-BzBExOxu.js} +1 -1
- package/.output/ui/assets/{index-CtB0deSZ.js → index-k6sRQz55.js} +1 -1
- package/.output/ui/assets/{json-viewer-Db0AFp-7.js → json-viewer-CWM4tY-j.js} +1 -1
- package/.output/ui/assets/{jszip.min-DkY0Ym_q.js → jszip.min-BkWvRFlo.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/package.json +8 -13
- package/src/assets/IDE/claudecode-color.svg +0 -1
- package/src/assets/IDE/codex-color.svg +0 -1
- package/src/assets/IDE/opencode.svg +0 -1
- package/src/assets/IDE/xiaomimimo.svg +0 -1
- package/src/assets/favicon.svg +0 -21
- package/src/assets/logos/alibaba.svg +0 -1
- package/src/assets/logos/anthropic.svg +0 -1
- package/src/assets/logos/claude-code.svg +0 -4
- package/src/assets/logos/deepseek.svg +0 -1
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +0 -1
- package/src/assets/logos/opencode.svg +0 -4
- package/src/assets/logos/qwen.webp +0 -0
- package/src/assets/logos/zhipuai.svg +0 -219
- package/src/backend/router.tsx +0 -36
- package/src/backend/routes/$.ts +0 -26
- package/src/backend/routes/__root.ts +0 -4
- package/src/backend/routes/api/-instances.ts +0 -7
- package/src/backend/routes/api/alerts.summary.ts +0 -28
- package/src/backend/routes/api/alerts.ts +0 -70
- package/src/backend/routes/api/config.paths.ts +0 -14
- package/src/backend/routes/api/config.ts +0 -73
- package/src/backend/routes/api/ecosystem.packages.$packageId.help.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.install.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.runner-presets.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.upgrade.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.ts +0 -21
- package/src/backend/routes/api/ecosystem.recipes.$recipeId.run.ts +0 -11
- package/src/backend/routes/api/ecosystem.tasks.$taskId.ts +0 -16
- package/src/backend/routes/api/ecosystem.tasks.ts +0 -12
- package/src/backend/routes/api/groups.$groupId.evidence.ts +0 -55
- package/src/backend/routes/api/groups.$groupId.sessions.ts +0 -50
- package/src/backend/routes/api/groups.$groupId.ts +0 -93
- package/src/backend/routes/api/groups.ts +0 -79
- package/src/backend/routes/api/health.ts +0 -22
- package/src/backend/routes/api/instances.$name.connection.ts +0 -12
- package/src/backend/routes/api/instances.$name.restart.ts +0 -23
- package/src/backend/routes/api/instances.$name.start.ts +0 -18
- package/src/backend/routes/api/instances.$name.stop.ts +0 -23
- package/src/backend/routes/api/instances.$name.ts +0 -12
- package/src/backend/routes/api/instances.ts +0 -16
- package/src/backend/routes/api/knowledge.candidates.$candidateId.promote.ts +0 -32
- package/src/backend/routes/api/knowledge.candidates.$candidateId.ts +0 -50
- package/src/backend/routes/api/knowledge.candidates.ts +0 -10
- package/src/backend/routes/api/knowledge.project-context.ts +0 -29
- package/src/backend/routes/api/knowledge.search.ts +0 -62
- package/src/backend/routes/api/knowledge.sessions.$sessionId.candidates.ts +0 -26
- package/src/backend/routes/api/logs.$id.body.ts +0 -58
- package/src/backend/routes/api/logs.$id.chunks.ts +0 -37
- package/src/backend/routes/api/logs.$id.replay.ts +0 -297
- package/src/backend/routes/api/logs.$id.ts +0 -23
- package/src/backend/routes/api/logs.import.ts +0 -39
- package/src/backend/routes/api/logs.stream.ts +0 -174
- package/src/backend/routes/api/logs.ts +0 -214
- package/src/backend/routes/api/mcp.ts +0 -25
- package/src/backend/routes/api/models.ts +0 -10
- package/src/backend/routes/api/pi-agent.ts +0 -29
- package/src/backend/routes/api/providers.$providerId.model-metadata.ts +0 -173
- package/src/backend/routes/api/providers.$providerId.test.log.ts +0 -342
- package/src/backend/routes/api/providers.$providerId.ts +0 -111
- package/src/backend/routes/api/providers.export.ts +0 -44
- package/src/backend/routes/api/providers.import.ts +0 -81
- package/src/backend/routes/api/providers.scan.ts +0 -28
- package/src/backend/routes/api/providers.ts +0 -102
- package/src/backend/routes/api/runs.$runId.evidence.ts +0 -55
- package/src/backend/routes/api/runs.$runId.ts +0 -50
- package/src/backend/routes/api/runs.ts +0 -62
- package/src/backend/routes/api/runtime.ts +0 -46
- package/src/backend/routes/api/sessions.ts +0 -51
- package/src/backend/routes/api/storage.ts +0 -56
- package/src/backend/routes/livez.ts +0 -13
- package/src/backend/routes/metrics.ts +0 -14
- package/src/backend/routes/proxy/$.ts +0 -15
- package/src/backend/routes/readyz.ts +0 -18
- package/src/backend/start.ts +0 -10
- package/src/cli/alias.ts +0 -431
- package/src/cli/detect-tools.ts +0 -123
- package/src/cli/doctor.ts +0 -836
- package/src/cli/instance.ts +0 -1071
- package/src/cli/instanceApi.ts +0 -213
- package/src/cli/instanceArgs.ts +0 -301
- package/src/cli/instanceControl.ts +0 -191
- package/src/cli/instanceModel.ts +0 -276
- package/src/cli/instanceRegistry.ts +0 -731
- package/src/cli/networkHints.ts +0 -170
- package/src/cli/onboard.ts +0 -1668
- package/src/cli/onboardTarget.ts +0 -228
- package/src/cli/rustBackendPackage.ts +0 -458
- package/src/cli/startupOutput.ts +0 -16
- package/src/cli/templates/codex-skill-onboard.ts +0 -320
- package/src/cli/templates/command-onboard.ts +0 -25
- package/src/cli/templates/skill-onboard.ts +0 -625
- package/src/cli.ts +0 -1397
- package/src/components/OnboardingBanner.tsx +0 -115
- package/src/components/ProxyViewer.tsx +0 -2259
- package/src/components/ProxyViewerContainer.tsx +0 -1076
- package/src/components/alerts/AlertsDialog.tsx +0 -615
- package/src/components/clients/ClientLogo.tsx +0 -175
- package/src/components/ecosystem/AgentLabDialog.tsx +0 -947
- package/src/components/errors/SafeErrorBoundary.tsx +0 -201
- package/src/components/groups/GroupsDialog.tsx +0 -1304
- package/src/components/inspector-pet/InspectorPet.tsx +0 -398
- package/src/components/inspector-pet/InspectorPetCrab3D.tsx +0 -455
- package/src/components/inspector-pet/inspectorPetLogic.ts +0 -272
- package/src/components/pi-agent/PiAgentPanel.tsx +0 -446
- package/src/components/pi-agent/piAgentChatLogic.ts +0 -273
- package/src/components/providers/ImportWizardDialog.tsx +0 -281
- package/src/components/providers/ProviderCard.tsx +0 -347
- package/src/components/providers/ProviderForm.tsx +0 -1183
- package/src/components/providers/ProviderLogo.tsx +0 -122
- package/src/components/providers/ProviderTestResultsView.tsx +0 -734
- package/src/components/providers/ProvidersPanel.tsx +0 -714
- package/src/components/providers/SettingsDialog.tsx +0 -1042
- package/src/components/providers/providerFormCapabilities.ts +0 -36
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +0 -902
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +0 -51
- package/src/components/proxy-viewer/ApplicationBar.tsx +0 -123
- package/src/components/proxy-viewer/CompareDrawer.tsx +0 -984
- package/src/components/proxy-viewer/ConversationGroup.tsx +0 -208
- package/src/components/proxy-viewer/ConversationGroupList.tsx +0 -652
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -429
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +0 -186
- package/src/components/proxy-viewer/LogEntry.tsx +0 -1419
- package/src/components/proxy-viewer/LogEntryHeader.tsx +0 -596
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -57
- package/src/components/proxy-viewer/RawExportWarning.tsx +0 -77
- package/src/components/proxy-viewer/ReplayDialog.tsx +0 -636
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +0 -299
- package/src/components/proxy-viewer/ResponseView.tsx +0 -202
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +0 -61
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +0 -265
- package/src/components/proxy-viewer/ThreadConnector.tsx +0 -165
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +0 -64
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -383
- package/src/components/proxy-viewer/TurnGroupList.tsx +0 -156
- package/src/components/proxy-viewer/accessibility.ts +0 -8
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +0 -887
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +0 -222
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +0 -737
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +0 -268
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +0 -53
- package/src/components/proxy-viewer/anatomy/types.ts +0 -41
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +0 -114
- package/src/components/proxy-viewer/bodyHydration.ts +0 -123
- package/src/components/proxy-viewer/cacheTrend.ts +0 -50
- package/src/components/proxy-viewer/diff/DiffView.tsx +0 -321
- package/src/components/proxy-viewer/diff/computeDiff.ts +0 -178
- package/src/components/proxy-viewer/diff/index.ts +0 -3
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +0 -194
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +0 -81
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +0 -21
- package/src/components/proxy-viewer/formats/index.tsx +0 -47
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +0 -442
- package/src/components/proxy-viewer/index.ts +0 -9
- package/src/components/proxy-viewer/lazy.ts +0 -41
- package/src/components/proxy-viewer/liveConnectionState.ts +0 -158
- package/src/components/proxy-viewer/log-formats/anthropic.ts +0 -191
- package/src/components/proxy-viewer/log-formats/index.ts +0 -23
- package/src/components/proxy-viewer/log-formats/openai.ts +0 -256
- package/src/components/proxy-viewer/log-formats/types.ts +0 -40
- package/src/components/proxy-viewer/log-formats/unknown.ts +0 -18
- package/src/components/proxy-viewer/logEntryVisibility.ts +0 -73
- package/src/components/proxy-viewer/logFocus.ts +0 -54
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +0 -124
- package/src/components/proxy-viewer/proxyViewerLogic.ts +0 -488
- package/src/components/proxy-viewer/replayComparison.ts +0 -131
- package/src/components/proxy-viewer/requestDiff.ts +0 -277
- package/src/components/proxy-viewer/requestTools.ts +0 -220
- package/src/components/proxy-viewer/useCopyFeedback.ts +0 -38
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +0 -232
- package/src/components/proxy-viewer/viewerState.ts +0 -176
- package/src/components/ui/badge.tsx +0 -47
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/collapsible.tsx +0 -21
- package/src/components/ui/confirm-dialog.tsx +0 -51
- package/src/components/ui/crab-logo.tsx +0 -45
- package/src/components/ui/crab-variants.tsx +0 -467
- package/src/components/ui/dialog.tsx +0 -130
- package/src/components/ui/icon-trigger.ts +0 -2
- package/src/components/ui/json-viewer-bulk.ts +0 -132
- package/src/components/ui/json-viewer.tsx +0 -508
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/select.tsx +0 -178
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -88
- package/src/components/ui/tooltip.tsx +0 -51
- package/src/components/ui/transient-toast.tsx +0 -105
- package/src/contracts/anthropic.ts +0 -230
- package/src/contracts/index.ts +0 -59
- package/src/contracts/json.ts +0 -37
- package/src/contracts/log.ts +0 -139
- package/src/contracts/openai.ts +0 -267
- package/src/index.css +0 -11
- package/src/knowledge/candidateStore.ts +0 -94
- package/src/knowledge/distiller.ts +0 -98
- package/src/knowledge/openclawClient.ts +0 -161
- package/src/knowledge/openclawGatewayClient.ts +0 -249
- package/src/knowledge/openclawMarkdown.ts +0 -146
- package/src/knowledge/redactor.ts +0 -80
- package/src/knowledge/types.ts +0 -83
- package/src/lib/alertContract.ts +0 -76
- package/src/lib/apiClient.ts +0 -166
- package/src/lib/apiFormat.ts +0 -29
- package/src/lib/backendImplementation.ts +0 -16
- package/src/lib/basePath.ts +0 -107
- package/src/lib/browserRuntimeContract.ts +0 -124
- package/src/lib/browserRuntimeResolver.ts +0 -333
- package/src/lib/chunkLoadRecovery.ts +0 -83
- package/src/lib/claudeCodeStrip.ts +0 -98
- package/src/lib/clipboard.ts +0 -67
- package/src/lib/codingAgentEndpoints.ts +0 -54
- package/src/lib/ecosystemContract.ts +0 -130
- package/src/lib/export-logs.ts +0 -287
- package/src/lib/groupContract.ts +0 -166
- package/src/lib/instanceContract.ts +0 -239
- package/src/lib/jsonFile.ts +0 -46
- package/src/lib/logImportContract.ts +0 -12
- package/src/lib/managedInstance.ts +0 -112
- package/src/lib/mask.ts +0 -4
- package/src/lib/objectUtils.ts +0 -22
- package/src/lib/piAgentContract.ts +0 -102
- package/src/lib/providerContract.ts +0 -125
- package/src/lib/providerImportContract.ts +0 -27
- package/src/lib/providerModelMetadata.ts +0 -458
- package/src/lib/providerTestContract.ts +0 -170
- package/src/lib/providerTestPrompt.ts +0 -100
- package/src/lib/publicBasePathContext.ts +0 -137
- package/src/lib/resourceLimits.ts +0 -345
- package/src/lib/runContract.ts +0 -164
- package/src/lib/runtimeConfig.ts +0 -43
- package/src/lib/safeDiagnostic.ts +0 -38
- package/src/lib/safeFetch.ts +0 -508
- package/src/lib/serverPort.ts +0 -47
- package/src/lib/sessionInfoContract.ts +0 -78
- package/src/lib/sessionUrl.ts +0 -46
- package/src/lib/ssrfGuard.ts +0 -133
- package/src/lib/stopReason.ts +0 -130
- package/src/lib/themeMode.ts +0 -41
- package/src/lib/timeDisplay.ts +0 -22
- package/src/lib/toolTrace.ts +0 -290
- package/src/lib/uiScale.ts +0 -117
- package/src/lib/unknown.ts +0 -27
- package/src/lib/upstreamUrl.ts +0 -150
- package/src/lib/useAlerts.ts +0 -82
- package/src/lib/useGroupEvidence.ts +0 -37
- package/src/lib/useGroups.ts +0 -32
- package/src/lib/useOnboarding.ts +0 -93
- package/src/lib/useProviders.ts +0 -30
- package/src/lib/useStripConfig.ts +0 -154
- package/src/lib/useThemeMode.ts +0 -40
- package/src/lib/useUiScale.ts +0 -52
- package/src/lib/utils.ts +0 -78
- package/src/mcp/currentContext.ts +0 -65
- package/src/mcp/instanceHandlers.ts +0 -151
- package/src/mcp/loopback.ts +0 -88
- package/src/mcp/mode.ts +0 -75
- package/src/mcp/previewExtractor.ts +0 -233
- package/src/mcp/server.ts +0 -2274
- package/src/mcp/toolHandlers.ts +0 -1533
- package/src/proxy/alerts.ts +0 -664
- package/src/proxy/chunkStorage.ts +0 -123
- package/src/proxy/config.ts +0 -218
- package/src/proxy/constants.ts +0 -66
- package/src/proxy/dataDir.ts +0 -147
- package/src/proxy/ecosystemExecutionConfirmation.ts +0 -114
- package/src/proxy/ecosystemExecutionRoute.ts +0 -116
- package/src/proxy/ecosystemRegistry.ts +0 -296
- package/src/proxy/ecosystemTasks.ts +0 -415
- package/src/proxy/evidenceAnalysis.ts +0 -526
- package/src/proxy/evidenceExporter.ts +0 -235
- package/src/proxy/formats/anthropic/anthropicProvider.ts +0 -75
- package/src/proxy/formats/anthropic/handler.ts +0 -75
- package/src/proxy/formats/anthropic/index.ts +0 -13
- package/src/proxy/formats/anthropic/register.ts +0 -4
- package/src/proxy/formats/anthropic/schemas.ts +0 -6
- package/src/proxy/formats/anthropic/stream.ts +0 -205
- package/src/proxy/formats/handler.ts +0 -46
- package/src/proxy/formats/index.ts +0 -13
- package/src/proxy/formats/jsonSchema.ts +0 -2
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -34
- package/src/proxy/formats/openai/codexMetadata.ts +0 -61
- package/src/proxy/formats/openai/handler.ts +0 -133
- package/src/proxy/formats/openai/index.ts +0 -33
- package/src/proxy/formats/openai/provider.ts +0 -50
- package/src/proxy/formats/openai/register.ts +0 -4
- package/src/proxy/formats/openai/schemas.ts +0 -25
- package/src/proxy/formats/openai/stream.ts +0 -600
- package/src/proxy/formats/openai/zhipuProvider.ts +0 -31
- package/src/proxy/formats/providerAdapter.ts +0 -50
- package/src/proxy/formats/providerRegistry.ts +0 -51
- package/src/proxy/formats/providers/index.ts +0 -3
- package/src/proxy/formats/registry.ts +0 -65
- package/src/proxy/groupEvidenceExporter.ts +0 -371
- package/src/proxy/groupStore.ts +0 -336
- package/src/proxy/handler.ts +0 -729
- package/src/proxy/identityProxy.ts +0 -1842
- package/src/proxy/jsonlRecovery.ts +0 -554
- package/src/proxy/logBodyChunks.ts +0 -91
- package/src/proxy/logFinalizer.ts +0 -777
- package/src/proxy/logFinalizer.worker.ts +0 -24
- package/src/proxy/logImportUpload.ts +0 -128
- package/src/proxy/logImporter.ts +0 -813
- package/src/proxy/logIndex.ts +0 -663
- package/src/proxy/logSearch.ts +0 -122
- package/src/proxy/logger.ts +0 -537
- package/src/proxy/openaiOrphanToolStrip.ts +0 -142
- package/src/proxy/platformCommands.ts +0 -253
- package/src/proxy/privateDataPath.ts +0 -419
- package/src/proxy/providerImporters.ts +0 -578
- package/src/proxy/providerScanStore.ts +0 -87
- package/src/proxy/providerSecretStore.ts +0 -216
- package/src/proxy/providers.ts +0 -998
- package/src/proxy/rawStreamCapture.ts +0 -342
- package/src/proxy/runFailures.ts +0 -94
- package/src/proxy/runStore.ts +0 -214
- package/src/proxy/runtimeAdmission.ts +0 -55
- package/src/proxy/runtimeHealth.ts +0 -221
- package/src/proxy/runtimeMetrics.ts +0 -149
- package/src/proxy/runtimeShutdown.ts +0 -157
- package/src/proxy/schemas.ts +0 -112
- package/src/proxy/sessionArchive.ts +0 -479
- package/src/proxy/sessionInfo.ts +0 -266
- package/src/proxy/sessionProcess.ts +0 -194
- package/src/proxy/sessionRuntime.ts +0 -121
- package/src/proxy/sessionSupervisor.ts +0 -305
- package/src/proxy/sessionWorkerEntry.ts +0 -26
- package/src/proxy/shutdownCoordinator.ts +0 -90
- package/src/proxy/socketTracker.ts +0 -639
- package/src/proxy/sqliteLogIndex.ts +0 -733
- package/src/proxy/storageLifecycle.ts +0 -432
- package/src/proxy/store.ts +0 -1773
- package/src/proxy/streamTiming.ts +0 -31
- package/src/proxy/toolSchemaWarnings.ts +0 -529
- package/src/proxy/uiAssetServer.ts +0 -106
- package/src/proxy/upstream.ts +0 -127
- package/src/services/alerts.ts +0 -12
- package/src/services/groups.ts +0 -119
- package/src/services/piAgent.ts +0 -421
- package/src/services/piAgentRetrieval.ts +0 -1180
- package/src/services/result.ts +0 -17
- package/src/services/runs.ts +0 -76
- package/src/ui/bootstrap.ts +0 -167
- package/src/ui/index.html +0 -38
- package/src/ui/logsStreamClient.ts +0 -20
- package/src/ui/main.tsx +0 -13
- package/src/ui/public/runtime-config.js +0 -2
- package/src/ui/router.tsx +0 -13
- package/src/ui/routes/__root.tsx +0 -69
- package/src/ui/routes/index.tsx +0 -35
- package/src/ui/routes/session/$sessionId.tsx +0 -50
- package/src/ui/runtimeDiscovery.ts +0 -50
package/src/proxy/handler.ts
DELETED
|
@@ -1,729 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
import { createLog, getNextLogId, persistFinalizedLogUpdate, type CapturedLog } from "./store";
|
|
3
|
-
import { appendLogEntry, logger } from "./logger";
|
|
4
|
-
import { extractRequestMetadata } from "./schemas";
|
|
5
|
-
import { findProviderByModel, type ProviderConfig } 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
|
-
SENSITIVE_HEADERS,
|
|
19
|
-
STATUS_FORBIDDEN,
|
|
20
|
-
STATUS_BAD_GATEWAY,
|
|
21
|
-
STATUS_PAYLOAD_TOO_LARGE,
|
|
22
|
-
MAX_REQUEST_BODY_BYTES,
|
|
23
|
-
} from "./constants";
|
|
24
|
-
import { getConfig } from "./config";
|
|
25
|
-
import { safeFetch } from "../lib/safeFetch";
|
|
26
|
-
|
|
27
|
-
const ALLOW_LOOPBACK = process.env["AGENT_INSPECTOR_ALLOW_LOOPBACK"] === "1";
|
|
28
|
-
const EMPTY_UPSTREAM_BODY_ERROR = "Upstream response did not include a body";
|
|
29
|
-
const UPSTREAM_STREAM_ERROR = "Upstream stream terminated unexpectedly";
|
|
30
|
-
const UPSTREAM_REQUEST_FAILED = "Proxy error: upstream request failed";
|
|
31
|
-
const HEADER_ERROR_CODE = "x-agent-inspector-error-code";
|
|
32
|
-
import { stripClaudeCodeBillingHeader } from "../lib/claudeCodeStrip";
|
|
33
|
-
import { stripOpenAIOrphanToolMessages } from "./openaiOrphanToolStrip";
|
|
34
|
-
import { maskApiKey } from "../lib/mask";
|
|
35
|
-
import { buildFileLogEntry, type FinalizeLogJob } from "./logFinalizer";
|
|
36
|
-
import { enqueueFinalizeLogJob } from "./sessionRuntime";
|
|
37
|
-
import { RawStreamCapture } from "./rawStreamCapture";
|
|
38
|
-
import {
|
|
39
|
-
acquireRuntimeRequest,
|
|
40
|
-
getRuntimeAdmissionState,
|
|
41
|
-
type RuntimeRequestLease,
|
|
42
|
-
} from "./runtimeAdmission";
|
|
43
|
-
import { DEFAULT_RUNTIME_BUDGET_POLICY, resourceLimitResponse } from "../lib/resourceLimits";
|
|
44
|
-
import {
|
|
45
|
-
buildUpstreamUrl,
|
|
46
|
-
describeApiRoute,
|
|
47
|
-
getProxyApiPath,
|
|
48
|
-
injectProviderAuth,
|
|
49
|
-
providerSupportsApiRoute,
|
|
50
|
-
selectUpstreamBase,
|
|
51
|
-
setUpstreamHost,
|
|
52
|
-
} from "./upstream";
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Strips all custom/non-standard headers from the request and replaces with
|
|
56
|
-
* unified proxy identity. Only preserves standard HTTP headers needed for API calls.
|
|
57
|
-
*/
|
|
58
|
-
function buildProxyHeaders(
|
|
59
|
-
originalHeaders: Headers,
|
|
60
|
-
captureRawHeaders: boolean,
|
|
61
|
-
): {
|
|
62
|
-
headers: Headers;
|
|
63
|
-
rawHeaders: Record<string, string> | undefined;
|
|
64
|
-
} {
|
|
65
|
-
const rawHeaders: Record<string, string> | undefined = captureRawHeaders ? {} : undefined;
|
|
66
|
-
const headers = new Headers();
|
|
67
|
-
|
|
68
|
-
if (rawHeaders !== undefined) {
|
|
69
|
-
originalHeaders.forEach((value, key) => {
|
|
70
|
-
const lowerKey = key.toLowerCase();
|
|
71
|
-
rawHeaders[lowerKey] = SENSITIVE_HEADERS.has(lowerKey) ? maskApiKey(value) : value;
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
headers.set(HEADER_USER_AGENT, PROXY_IDENTITY);
|
|
76
|
-
headers.set(HEADER_X_PROXY_IDENTITY, PROXY_IDENTITY);
|
|
77
|
-
|
|
78
|
-
for (const name of PRESERVE_HEADERS) {
|
|
79
|
-
const value = originalHeaders.get(name);
|
|
80
|
-
if (value !== null) {
|
|
81
|
-
headers.set(name, value);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return { headers, rawHeaders };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
type ParsedRequestPath = {
|
|
89
|
-
apiPath: string;
|
|
90
|
-
isMessages: boolean;
|
|
91
|
-
isChatCompletions: boolean;
|
|
92
|
-
normalizedPath: string;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
function parseRequestPath(req: Request, url: URL): ParsedRequestPath {
|
|
96
|
-
const route = describeApiRoute(getProxyApiPath(url));
|
|
97
|
-
const isPost = req.method === "POST";
|
|
98
|
-
const isChatCompletions = isPost && route.isChatCompletions;
|
|
99
|
-
const isMessages = isPost && (route.endpointPath === PATH_V1_MESSAGES || route.isChatCompletions);
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
apiPath: route.apiPath,
|
|
103
|
-
isMessages,
|
|
104
|
-
isChatCompletions,
|
|
105
|
-
normalizedPath: route.normalizedPath,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function errorMessage(err: unknown): string {
|
|
110
|
-
return err instanceof Error ? err.message : String(err);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function protocolLabel(route: ReturnType<typeof describeApiRoute>): string {
|
|
114
|
-
if (route.isResponses) return "openai-responses";
|
|
115
|
-
if (route.isChatCompletions) return "openai-chat-completions";
|
|
116
|
-
return "anthropic-messages";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function requiredProviderUrlField(route: ReturnType<typeof describeApiRoute>): string {
|
|
120
|
-
if (route.isResponses) return "openaiResponsesBaseUrl or openaiBaseUrl";
|
|
121
|
-
if (route.isChatCompletions) return "openaiBaseUrl";
|
|
122
|
-
return "anthropicBaseUrl";
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function configuredProviderUrlFields(provider: ProviderConfig): string[] {
|
|
126
|
-
const fields: string[] = [];
|
|
127
|
-
if (provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl.trim() !== "") {
|
|
128
|
-
fields.push("anthropicBaseUrl");
|
|
129
|
-
}
|
|
130
|
-
if (provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl.trim() !== "") {
|
|
131
|
-
fields.push("openaiBaseUrl");
|
|
132
|
-
}
|
|
133
|
-
if (
|
|
134
|
-
provider.openaiResponsesBaseUrl !== undefined &&
|
|
135
|
-
provider.openaiResponsesBaseUrl.trim() !== ""
|
|
136
|
-
) {
|
|
137
|
-
fields.push("openaiResponsesBaseUrl");
|
|
138
|
-
}
|
|
139
|
-
if (provider.baseUrl !== undefined && provider.baseUrl.trim() !== "") {
|
|
140
|
-
fields.push("baseUrl");
|
|
141
|
-
}
|
|
142
|
-
return fields;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
type UnsupportedProviderRoute = {
|
|
146
|
-
code: "request-model-missing" | "provider-not-found" | "provider-route-mismatch";
|
|
147
|
-
detail: string;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
function unsupportedProviderRoute(
|
|
151
|
-
model: string | null,
|
|
152
|
-
route: ReturnType<typeof describeApiRoute>,
|
|
153
|
-
provider: ProviderConfig | null,
|
|
154
|
-
): UnsupportedProviderRoute {
|
|
155
|
-
if (model === null) {
|
|
156
|
-
const detail = `model could not be extracted; path=${route.normalizedPath}; protocol=${protocolLabel(route)}`;
|
|
157
|
-
logger.warn(`[handler] Unsupported provider route: code=request-model-missing; ${detail}`);
|
|
158
|
-
return { code: "request-model-missing", detail };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (provider === null) {
|
|
162
|
-
const detail = `no Provider matched model=${model}; path=${route.normalizedPath}; protocol=${protocolLabel(route)}`;
|
|
163
|
-
logger.warn(`[handler] Unsupported provider route: code=provider-not-found; ${detail}`);
|
|
164
|
-
return { code: "provider-not-found", detail };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const configuredFields = configuredProviderUrlFields(provider);
|
|
168
|
-
const detail =
|
|
169
|
-
`Provider "${provider.name}" (${provider.id}) matched model=${model}, ` +
|
|
170
|
-
`but request path ${route.normalizedPath} requires ${protocolLabel(route)} via ` +
|
|
171
|
-
`${requiredProviderUrlField(route)}; configured URL fields=${
|
|
172
|
-
configuredFields.length > 0 ? configuredFields.join(",") : "none"
|
|
173
|
-
}`;
|
|
174
|
-
logger.warn(`[handler] Unsupported provider route: code=provider-route-mismatch; ${detail}`);
|
|
175
|
-
return { code: "provider-route-mismatch", detail };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
type BodyReadResult = { ok: true; body: string | null } | { ok: false; reason: "too-large" };
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Reads the request body as text, enforcing a maximum byte limit to prevent
|
|
182
|
-
* memory exhaustion from oversized payloads. Returns the body string, or null
|
|
183
|
-
* if the request has no body.
|
|
184
|
-
*/
|
|
185
|
-
async function readBodyWithLimit(req: Request, limitBytes: number): Promise<BodyReadResult> {
|
|
186
|
-
if (req.body === null || req.method === "GET" || req.method === "HEAD") {
|
|
187
|
-
return { ok: true, body: null };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Fast path: if Content-Length is present and exceeds the limit, reject
|
|
191
|
-
// without reading any bytes.
|
|
192
|
-
const contentLengthHeader = req.headers.get(HEADER_CONTENT_LENGTH);
|
|
193
|
-
if (contentLengthHeader !== null) {
|
|
194
|
-
const declared = Number.parseInt(contentLengthHeader, 10);
|
|
195
|
-
if (Number.isFinite(declared) && declared > limitBytes) {
|
|
196
|
-
return { ok: false, reason: "too-large" };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const reader = req.body.getReader();
|
|
201
|
-
const decoder = new TextDecoder();
|
|
202
|
-
const chunks: string[] = [];
|
|
203
|
-
let totalBytes = 0;
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
for (;;) {
|
|
207
|
-
const { done, value } = await reader.read();
|
|
208
|
-
if (done) break;
|
|
209
|
-
totalBytes += value.byteLength;
|
|
210
|
-
if (totalBytes > limitBytes) {
|
|
211
|
-
return { ok: false, reason: "too-large" };
|
|
212
|
-
}
|
|
213
|
-
chunks.push(decoder.decode(value, { stream: true }));
|
|
214
|
-
}
|
|
215
|
-
chunks.push(decoder.decode());
|
|
216
|
-
return { ok: true, body: chunks.join("") };
|
|
217
|
-
} finally {
|
|
218
|
-
reader.releaseLock();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function scheduleLogFinalization(job: FinalizeLogJob): void {
|
|
223
|
-
void enqueueFinalizeLogJob(job).catch((err) => {
|
|
224
|
-
logger.error(
|
|
225
|
-
`[handler] Session finalization task failed for log #${job.log.id}:`,
|
|
226
|
-
errorMessage(err),
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function handleNonStreamingResponse(
|
|
232
|
-
upstreamRes: Response,
|
|
233
|
-
responseBody: string,
|
|
234
|
-
startTime: number,
|
|
235
|
-
upstreamUrl: string,
|
|
236
|
-
log: CapturedLog,
|
|
237
|
-
): Response {
|
|
238
|
-
const elapsedMs = Date.now() - startTime;
|
|
239
|
-
|
|
240
|
-
scheduleLogFinalization({
|
|
241
|
-
type: "non-streaming",
|
|
242
|
-
log,
|
|
243
|
-
upstreamUrl,
|
|
244
|
-
elapsedMs,
|
|
245
|
-
responseStatus: upstreamRes.status,
|
|
246
|
-
responseBody,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const responseHeaders = new Headers(upstreamRes.headers);
|
|
250
|
-
responseHeaders.delete(HEADER_CONTENT_ENCODING);
|
|
251
|
-
responseHeaders.delete(HEADER_CONTENT_LENGTH);
|
|
252
|
-
|
|
253
|
-
return new Response(responseBody, {
|
|
254
|
-
status: upstreamRes.status,
|
|
255
|
-
headers: responseHeaders,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function applyRawStreamCaptureStatus(log: CapturedLog, capture: RawStreamCapture): void {
|
|
260
|
-
const status = capture.status;
|
|
261
|
-
log.captureIncomplete = status.incomplete;
|
|
262
|
-
log.droppedChunks = status.droppedChunks;
|
|
263
|
-
log.droppedBytes = status.droppedBytes;
|
|
264
|
-
log.captureIncompleteReason = status.reason;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function applyUpstreamStreamError(log: CapturedLog): void {
|
|
268
|
-
log.captureIncomplete = true;
|
|
269
|
-
log.captureIncompleteReason = "upstream-stream-error";
|
|
270
|
-
log.error = UPSTREAM_STREAM_ERROR;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async function handleStreamingResponse(
|
|
274
|
-
upstreamRes: Response,
|
|
275
|
-
req: Request,
|
|
276
|
-
startTime: number,
|
|
277
|
-
upstreamUrl: string,
|
|
278
|
-
log: CapturedLog,
|
|
279
|
-
collectStreamingChunks: boolean,
|
|
280
|
-
): Promise<Response> {
|
|
281
|
-
log.streaming = true;
|
|
282
|
-
log.responseStatus = upstreamRes.status;
|
|
283
|
-
|
|
284
|
-
if (upstreamRes.body === null) {
|
|
285
|
-
log.elapsedMs = Date.now() - startTime;
|
|
286
|
-
log.responseStatus = STATUS_BAD_GATEWAY;
|
|
287
|
-
log.responseText = EMPTY_UPSTREAM_BODY_ERROR;
|
|
288
|
-
log.captureIncomplete = true;
|
|
289
|
-
log.droppedChunks = 0;
|
|
290
|
-
log.droppedBytes = 0;
|
|
291
|
-
log.captureIncompleteReason = "upstream-stream-error";
|
|
292
|
-
log.error = EMPTY_UPSTREAM_BODY_ERROR;
|
|
293
|
-
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: EMPTY_UPSTREAM_BODY_ERROR });
|
|
294
|
-
await persistFinalizedLogUpdate(log);
|
|
295
|
-
return new Response("No response body", { status: STATUS_BAD_GATEWAY });
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const capture = new RawStreamCapture();
|
|
299
|
-
const decoder = new TextDecoder();
|
|
300
|
-
let finalizationScheduled = false;
|
|
301
|
-
let firstChunkMs: number | null = null;
|
|
302
|
-
let pendingDecoderBytes = 0;
|
|
303
|
-
let downstreamCancelled = false;
|
|
304
|
-
|
|
305
|
-
const submitDecodedText = (text: string, flush: boolean = false): void => {
|
|
306
|
-
const observedBytes = flush
|
|
307
|
-
? pendingDecoderBytes
|
|
308
|
-
: Math.min(pendingDecoderBytes, Buffer.byteLength(text, "utf-8"));
|
|
309
|
-
pendingDecoderBytes -= observedBytes;
|
|
310
|
-
if (text === "" && observedBytes === 0) return;
|
|
311
|
-
capture.tryAppend(text, observedBytes);
|
|
312
|
-
applyRawStreamCaptureStatus(log, capture);
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const scheduleStreamingFinalization = (
|
|
316
|
-
type: "streaming" | "stream-abort",
|
|
317
|
-
upstreamStreamFailed: boolean = false,
|
|
318
|
-
): void => {
|
|
319
|
-
if (finalizationScheduled) return;
|
|
320
|
-
finalizationScheduled = true;
|
|
321
|
-
|
|
322
|
-
if (pendingDecoderBytes > 0) {
|
|
323
|
-
capture.recordDroppedObservation("pipeline-closed", pendingDecoderBytes);
|
|
324
|
-
pendingDecoderBytes = 0;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const totalStreamMs = Date.now() - startTime;
|
|
328
|
-
const elapsedMs = totalStreamMs;
|
|
329
|
-
void capture
|
|
330
|
-
.finalize()
|
|
331
|
-
.then((rawStream) => {
|
|
332
|
-
applyRawStreamCaptureStatus(log, capture);
|
|
333
|
-
if (upstreamStreamFailed) applyUpstreamStreamError(log);
|
|
334
|
-
switch (type) {
|
|
335
|
-
case "streaming":
|
|
336
|
-
scheduleLogFinalization({
|
|
337
|
-
type: "streaming",
|
|
338
|
-
log,
|
|
339
|
-
upstreamUrl,
|
|
340
|
-
elapsedMs,
|
|
341
|
-
responseStatus: upstreamRes.status,
|
|
342
|
-
rawStream,
|
|
343
|
-
collectStreamingChunks,
|
|
344
|
-
firstChunkMs,
|
|
345
|
-
totalStreamMs,
|
|
346
|
-
terminalError: upstreamStreamFailed ? UPSTREAM_STREAM_ERROR : null,
|
|
347
|
-
});
|
|
348
|
-
return;
|
|
349
|
-
case "stream-abort":
|
|
350
|
-
scheduleLogFinalization({
|
|
351
|
-
type: "stream-abort",
|
|
352
|
-
log,
|
|
353
|
-
upstreamUrl,
|
|
354
|
-
elapsedMs,
|
|
355
|
-
rawStream,
|
|
356
|
-
hasChunks: capture.hasChunks,
|
|
357
|
-
collectStreamingChunks,
|
|
358
|
-
firstChunkMs,
|
|
359
|
-
totalStreamMs,
|
|
360
|
-
});
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
})
|
|
364
|
-
.catch((error: unknown) => {
|
|
365
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
366
|
-
applyRawStreamCaptureStatus(log, capture);
|
|
367
|
-
if (upstreamStreamFailed) applyUpstreamStreamError(log);
|
|
368
|
-
log.captureIncomplete = true;
|
|
369
|
-
log.captureIncompleteReason ??= "finalize-failed";
|
|
370
|
-
logger.error(
|
|
371
|
-
`[handler] Raw stream finalization failed for log #${String(log.id)}:`,
|
|
372
|
-
message,
|
|
373
|
-
);
|
|
374
|
-
scheduleLogFinalization({
|
|
375
|
-
type: "stream-abort",
|
|
376
|
-
log,
|
|
377
|
-
upstreamUrl,
|
|
378
|
-
elapsedMs,
|
|
379
|
-
rawStream: { type: "memory", rawStream: "" },
|
|
380
|
-
hasChunks: capture.hasChunks,
|
|
381
|
-
collectStreamingChunks,
|
|
382
|
-
firstChunkMs,
|
|
383
|
-
totalStreamMs,
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
389
|
-
transform(chunk, controller) {
|
|
390
|
-
if (firstChunkMs === null) firstChunkMs = Date.now() - startTime;
|
|
391
|
-
controller.enqueue(chunk);
|
|
392
|
-
pendingDecoderBytes += chunk.byteLength;
|
|
393
|
-
submitDecodedText(decoder.decode(chunk, { stream: true }));
|
|
394
|
-
},
|
|
395
|
-
flush() {
|
|
396
|
-
submitDecodedText(decoder.decode(), true);
|
|
397
|
-
scheduleStreamingFinalization("streaming");
|
|
398
|
-
},
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// pipeTo returns a promise that rejects when either side errors. Without
|
|
402
|
-
// this catch, an upstream connection drop skips flush() and leaves the log
|
|
403
|
-
// permanently un-finalized with a leaked temp file.
|
|
404
|
-
void upstreamRes.body.pipeTo(transform.writable).catch(() => {
|
|
405
|
-
if (downstreamCancelled || req.signal?.aborted === true) {
|
|
406
|
-
scheduleStreamingFinalization("stream-abort");
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
scheduleStreamingFinalization("streaming", true);
|
|
410
|
-
});
|
|
411
|
-
const transformedReader = transform.readable.getReader();
|
|
412
|
-
const loggedStream = new ReadableStream<Uint8Array>({
|
|
413
|
-
async pull(controller) {
|
|
414
|
-
try {
|
|
415
|
-
const next = await transformedReader.read();
|
|
416
|
-
if (next.done) {
|
|
417
|
-
controller.close();
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
controller.enqueue(next.value);
|
|
421
|
-
} catch (error) {
|
|
422
|
-
controller.error(error);
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
async cancel(reason) {
|
|
426
|
-
downstreamCancelled = true;
|
|
427
|
-
scheduleStreamingFinalization("stream-abort");
|
|
428
|
-
await transformedReader.cancel(reason);
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
if (req.signal === undefined) {
|
|
433
|
-
logger.warn("[handler] req.signal is undefined; abort detection disabled for this request");
|
|
434
|
-
} else {
|
|
435
|
-
req.signal.addEventListener("abort", () => {
|
|
436
|
-
if (log.responseText === null) {
|
|
437
|
-
logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
|
|
438
|
-
scheduleStreamingFinalization("stream-abort");
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const responseHeaders = new Headers(upstreamRes.headers);
|
|
444
|
-
responseHeaders.delete(HEADER_CONTENT_ENCODING);
|
|
445
|
-
responseHeaders.delete(HEADER_CONTENT_LENGTH);
|
|
446
|
-
|
|
447
|
-
return new Response(loggedStream, {
|
|
448
|
-
status: upstreamRes.status,
|
|
449
|
-
headers: responseHeaders,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function handleAdmittedProxy(req: Request): Promise<Response> {
|
|
454
|
-
const url = new URL(req.url);
|
|
455
|
-
const parsed = parseRequestPath(req, url);
|
|
456
|
-
|
|
457
|
-
// Read body with a size limit to prevent memory exhaustion from oversized
|
|
458
|
-
// payloads. The limit is generous (50 MB) — legitimate LLM API requests are
|
|
459
|
-
// well under this, but a malicious client could send a multi-GB body to OOM
|
|
460
|
-
// the proxy.
|
|
461
|
-
const bodyRead = await readBodyWithLimit(req, MAX_REQUEST_BODY_BYTES);
|
|
462
|
-
if (!bodyRead.ok) {
|
|
463
|
-
logger.warn(`[handler] Rejected oversized request body: ${req.method} ${parsed.apiPath}`);
|
|
464
|
-
return new Response("Request body too large", { status: STATUS_PAYLOAD_TOO_LARGE });
|
|
465
|
-
}
|
|
466
|
-
const requestBody = bodyRead.body;
|
|
467
|
-
|
|
468
|
-
// Opt-in: strip Claude Code's synthetic billing-header text block from the
|
|
469
|
-
// system prompt. Only runs for Anthropic-format requests (those whose path
|
|
470
|
-
// resolves to /v1/messages). The flag is read once per request from the
|
|
471
|
-
// runtime config, so a PATCH to /api/config takes effect on the next
|
|
472
|
-
// request without a restart.
|
|
473
|
-
//
|
|
474
|
-
// Important: the original `requestBody` must be preserved so the log stores
|
|
475
|
-
// the unmodified request (the "Raw Request" tab needs the true bytes the
|
|
476
|
-
// client sent). The rewritten body is only used for the upstream fetch.
|
|
477
|
-
const config = getConfig();
|
|
478
|
-
let bodyToForward = requestBody;
|
|
479
|
-
if (config.stripClaudeCodeBillingHeader && requestBody !== null && parsed.isMessages) {
|
|
480
|
-
const stripped = stripClaudeCodeBillingHeader(requestBody);
|
|
481
|
-
if (stripped.removed > 0) {
|
|
482
|
-
logger.info(
|
|
483
|
-
`[handler] Stripped ${stripped.removed} Claude Code billing-header block(s) from system prompt`,
|
|
484
|
-
);
|
|
485
|
-
bodyToForward = stripped.body;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Always-on OpenAI Chat Completions format guard: every
|
|
490
|
-
// `role: "tool"` message to reference a `tool_call_id` present in a
|
|
491
|
-
// preceding assistant message's `tool_calls`. Buggy clients sometimes
|
|
492
|
-
// record a tool result without the matching assistant tool_call, which
|
|
493
|
-
// makes the upstream reject the request with a 400 ("tool result's tool
|
|
494
|
-
// id(call_xxx) not found"). Drop the orphan messages so the proxy can
|
|
495
|
-
// forward a valid request; the original body is still kept in the log.
|
|
496
|
-
if (bodyToForward !== null && parsed.isChatCompletions) {
|
|
497
|
-
const stripped = stripOpenAIOrphanToolMessages(bodyToForward);
|
|
498
|
-
if (stripped.removed > 0) {
|
|
499
|
-
logger.warn(
|
|
500
|
-
`[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`,
|
|
501
|
-
);
|
|
502
|
-
bodyToForward = stripped.body;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Parse the request body exactly once. The handler needs the model for
|
|
507
|
-
// provider routing and the session id for the log; `createLog` would
|
|
508
|
-
// otherwise re-parse the same body to extract the same fields.
|
|
509
|
-
const { model, sessionId } = extractRequestMetadata(requestBody, req.headers);
|
|
510
|
-
|
|
511
|
-
// Find provider config using already-extracted model (not calling extractModelFromBody again)
|
|
512
|
-
const matchedProviderConfig = model !== null ? findProviderByModel(model) : null;
|
|
513
|
-
const route = describeApiRoute(parsed.apiPath);
|
|
514
|
-
|
|
515
|
-
// Model matching is necessary but not sufficient: the Provider must explicitly expose the
|
|
516
|
-
// protocol selected by the request path. This guard stays before log creation and fetch so a
|
|
517
|
-
// protocol mismatch has the same zero-side-effect contract as an unconfigured model.
|
|
518
|
-
if (
|
|
519
|
-
model === null ||
|
|
520
|
-
matchedProviderConfig === null ||
|
|
521
|
-
!providerSupportsApiRoute(route, matchedProviderConfig)
|
|
522
|
-
) {
|
|
523
|
-
const unsupported = unsupportedProviderRoute(model, route, matchedProviderConfig);
|
|
524
|
-
return new Response("Forbidden: unsupported provider", {
|
|
525
|
-
status: STATUS_FORBIDDEN,
|
|
526
|
-
headers: {
|
|
527
|
-
[HEADER_ERROR_CODE]: unsupported.code,
|
|
528
|
-
},
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const upstreamBase = selectUpstreamBase(route, matchedProviderConfig);
|
|
533
|
-
const upstreamUrl = buildUpstreamUrl(upstreamBase, parsed.normalizedPath);
|
|
534
|
-
|
|
535
|
-
const startTime = Date.now();
|
|
536
|
-
|
|
537
|
-
const captureFullDetails = config.captureMode === "full";
|
|
538
|
-
const { headers: upstreamHeaders, rawHeaders } = buildProxyHeaders(
|
|
539
|
-
req.headers,
|
|
540
|
-
captureFullDetails,
|
|
541
|
-
);
|
|
542
|
-
setUpstreamHost(upstreamHeaders, upstreamBase);
|
|
543
|
-
injectProviderAuth(upstreamHeaders, matchedProviderConfig);
|
|
544
|
-
|
|
545
|
-
// Pick the format handler from the request path. ProviderConfig.format is only a
|
|
546
|
-
// hint used as a fallback when no anthropicBaseUrl/openaiBaseUrl is set, and
|
|
547
|
-
// does NOT describe what the upstream actually returns: a provider configured
|
|
548
|
-
// with format="openai" may still expose an Anthropic-compatible endpoint
|
|
549
|
-
// (e.g. MiniMax's /anthropic path), and the response shape always follows
|
|
550
|
-
// the URL path the client hit, not the provider.format label.
|
|
551
|
-
const formatHandler = formatForPath(parsed.apiPath);
|
|
552
|
-
if (formatHandler === null) {
|
|
553
|
-
return new Response("Forbidden: unsupported format", { status: STATUS_FORBIDDEN });
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Acquire client info and the next log id in parallel. On Windows
|
|
557
|
-
// `getClientInfo` shells out to PowerShell and is the single slowest
|
|
558
|
-
// thing on the hot path; serializing it before `createLog` paid the
|
|
559
|
-
// full PowerShell round-trip on top of everything else before
|
|
560
|
-
// `fetch` is even issued.
|
|
561
|
-
const [clientInfo, preAcquiredId] = await Promise.all([getClientInfo(req), getNextLogId()]);
|
|
562
|
-
const upstreamHeadersObj: Record<string, string> | undefined = captureFullDetails
|
|
563
|
-
? {}
|
|
564
|
-
: undefined;
|
|
565
|
-
if (upstreamHeadersObj !== undefined) {
|
|
566
|
-
upstreamHeaders.forEach((value, key) => {
|
|
567
|
-
const lowerKey = key.toLowerCase();
|
|
568
|
-
upstreamHeadersObj[lowerKey] = SENSITIVE_HEADERS.has(lowerKey) ? maskApiKey(value) : value;
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
const log = await createLog(
|
|
572
|
-
req.method,
|
|
573
|
-
parsed.apiPath,
|
|
574
|
-
requestBody,
|
|
575
|
-
req.headers,
|
|
576
|
-
clientInfo,
|
|
577
|
-
rawHeaders,
|
|
578
|
-
upstreamHeadersObj,
|
|
579
|
-
formatHandler.format,
|
|
580
|
-
model,
|
|
581
|
-
sessionId,
|
|
582
|
-
preAcquiredId,
|
|
583
|
-
);
|
|
584
|
-
|
|
585
|
-
let upstreamRes: Response;
|
|
586
|
-
try {
|
|
587
|
-
const fetchResult = await safeFetch(
|
|
588
|
-
upstreamUrl,
|
|
589
|
-
{
|
|
590
|
-
method: req.method,
|
|
591
|
-
headers: upstreamHeaders,
|
|
592
|
-
body: bodyToForward,
|
|
593
|
-
signal: req.signal,
|
|
594
|
-
},
|
|
595
|
-
{
|
|
596
|
-
allowLoopback: ALLOW_LOOPBACK,
|
|
597
|
-
deadlineMs: DEFAULT_RUNTIME_BUDGET_POLICY.upstream.fetchDeadlineMs,
|
|
598
|
-
responseBytes: DEFAULT_RUNTIME_BUDGET_POLICY.upstream.fetchResponseBytes,
|
|
599
|
-
},
|
|
600
|
-
);
|
|
601
|
-
if (!fetchResult.ok) {
|
|
602
|
-
const status =
|
|
603
|
-
fetchResult.kind === "network-policy"
|
|
604
|
-
? STATUS_FORBIDDEN
|
|
605
|
-
: fetchResult.kind === "resource-limit"
|
|
606
|
-
? STATUS_PAYLOAD_TOO_LARGE
|
|
607
|
-
: STATUS_BAD_GATEWAY;
|
|
608
|
-
logger.warn(`[handler] Safe fetch blocked ${upstreamUrl}: ${fetchResult.reason}`);
|
|
609
|
-
log.elapsedMs = Date.now() - startTime;
|
|
610
|
-
log.responseStatus = status;
|
|
611
|
-
log.responseText = fetchResult.reason;
|
|
612
|
-
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: fetchResult.reason });
|
|
613
|
-
await persistFinalizedLogUpdate(log);
|
|
614
|
-
return new Response(
|
|
615
|
-
fetchResult.kind === "network-policy"
|
|
616
|
-
? "Forbidden: upstream URL is not permitted"
|
|
617
|
-
: fetchResult.kind === "resource-limit"
|
|
618
|
-
? "Proxy error: upstream response exceeded configured limits"
|
|
619
|
-
: fetchResult.kind === "timeout"
|
|
620
|
-
? "Proxy error: upstream request timed out"
|
|
621
|
-
: "Proxy error: upstream redirects are not permitted",
|
|
622
|
-
{ status },
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
upstreamRes = fetchResult.response;
|
|
626
|
-
} catch (err) {
|
|
627
|
-
log.elapsedMs = Date.now() - startTime;
|
|
628
|
-
// Check if it was a client abort (not a proxy error)
|
|
629
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
630
|
-
logger.info(`[handler] Client aborted: ${req.method} ${parsed.apiPath}`);
|
|
631
|
-
log.responseStatus = 499; // Client Closed Request (non-standard but descriptive)
|
|
632
|
-
log.responseText = "Client aborted";
|
|
633
|
-
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
|
|
634
|
-
await persistFinalizedLogUpdate(log);
|
|
635
|
-
return new Response("Client aborted", { status: 499 });
|
|
636
|
-
}
|
|
637
|
-
const upstreamError = errorMessage(err);
|
|
638
|
-
logger.error(`[handler] Proxy error: ${req.method} ${parsed.apiPath}`, upstreamError);
|
|
639
|
-
log.responseStatus = STATUS_BAD_GATEWAY;
|
|
640
|
-
log.responseText = upstreamError;
|
|
641
|
-
log.error = upstreamError;
|
|
642
|
-
appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: upstreamError });
|
|
643
|
-
await persistFinalizedLogUpdate(log);
|
|
644
|
-
return new Response(UPSTREAM_REQUEST_FAILED, { status: STATUS_BAD_GATEWAY });
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const isStream =
|
|
648
|
-
upstreamRes.headers.get(HEADER_CONTENT_TYPE)?.includes(CONTENT_TYPE_EVENT_STREAM) ?? false;
|
|
649
|
-
|
|
650
|
-
if (!isStream) {
|
|
651
|
-
const responseBody = await upstreamRes.text();
|
|
652
|
-
return handleNonStreamingResponse(upstreamRes, responseBody, startTime, upstreamUrl, log);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return handleStreamingResponse(upstreamRes, req, startTime, upstreamUrl, log, captureFullDetails);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function responseWithRuntimeLease(response: Response, lease: RuntimeRequestLease): Response {
|
|
659
|
-
if (response.body === null) {
|
|
660
|
-
lease.release();
|
|
661
|
-
return response;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const reader = response.body.getReader();
|
|
665
|
-
let released = false;
|
|
666
|
-
const release = (): void => {
|
|
667
|
-
if (released) return;
|
|
668
|
-
released = true;
|
|
669
|
-
lease.release();
|
|
670
|
-
};
|
|
671
|
-
const trackedBody = new ReadableStream<Uint8Array>({
|
|
672
|
-
async pull(controller) {
|
|
673
|
-
try {
|
|
674
|
-
const chunk = await reader.read();
|
|
675
|
-
if (chunk.done) {
|
|
676
|
-
controller.close();
|
|
677
|
-
release();
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
controller.enqueue(chunk.value);
|
|
681
|
-
} catch (error) {
|
|
682
|
-
controller.error(error);
|
|
683
|
-
release();
|
|
684
|
-
}
|
|
685
|
-
},
|
|
686
|
-
async cancel(reason) {
|
|
687
|
-
try {
|
|
688
|
-
await reader.cancel(reason);
|
|
689
|
-
} finally {
|
|
690
|
-
release();
|
|
691
|
-
}
|
|
692
|
-
},
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
return new Response(trackedBody, {
|
|
696
|
-
status: response.status,
|
|
697
|
-
statusText: response.statusText,
|
|
698
|
-
headers: response.headers,
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
export async function handleProxy(req: Request): Promise<Response> {
|
|
703
|
-
const lease = acquireRuntimeRequest();
|
|
704
|
-
if (lease === null) {
|
|
705
|
-
const admission = getRuntimeAdmissionState();
|
|
706
|
-
if (admission.acceptingRequests) {
|
|
707
|
-
return resourceLimitResponse({
|
|
708
|
-
error: `Proxy request capacity reached. At most ${String(admission.capacity)} active requests are allowed; retry after a response completes.`,
|
|
709
|
-
code: "proxy-capacity-exceeded",
|
|
710
|
-
status: 503,
|
|
711
|
-
resource: "proxy-requests",
|
|
712
|
-
limit: admission.capacity,
|
|
713
|
-
retryable: true,
|
|
714
|
-
retryAfterSeconds: 1,
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
return Response.json(
|
|
718
|
-
{ error: "Agent Inspector is shutting down" },
|
|
719
|
-
{ status: 503, headers: { "cache-control": "no-store", "retry-after": "5" } },
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
try {
|
|
724
|
-
return responseWithRuntimeLease(await handleAdmittedProxy(req), lease);
|
|
725
|
-
} catch (error) {
|
|
726
|
-
lease.release();
|
|
727
|
-
return await Promise.reject(error);
|
|
728
|
-
}
|
|
729
|
-
}
|