@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/providers.ts
DELETED
|
@@ -1,998 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import Conf from "conf";
|
|
3
|
-
import { randomUUID } from "crypto";
|
|
4
|
-
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { maskApiKey } from "../lib/mask";
|
|
7
|
-
import { logger } from "./logger";
|
|
8
|
-
import { getDataDir, hasExplicitDataDir } from "./dataDir";
|
|
9
|
-
import {
|
|
10
|
-
ProviderConfigSchema,
|
|
11
|
-
ProviderModelMetadataListSchema,
|
|
12
|
-
ProviderModelNameSchema,
|
|
13
|
-
ProviderModelsSchema,
|
|
14
|
-
REDACTED_PROVIDER_API_KEY,
|
|
15
|
-
toRedactedProviderDto,
|
|
16
|
-
toRedactedProviderDtos,
|
|
17
|
-
type ProviderConfig,
|
|
18
|
-
type ProviderModelMetadata,
|
|
19
|
-
type RedactedProviderDto,
|
|
20
|
-
} from "../lib/providerContract";
|
|
21
|
-
import { withBuiltinProviderModelMetadata } from "../lib/providerModelMetadata";
|
|
22
|
-
import {
|
|
23
|
-
createProviderSecretStore,
|
|
24
|
-
secretValuesEqual,
|
|
25
|
-
type ProviderSecretStore,
|
|
26
|
-
} from "./providerSecretStore";
|
|
27
|
-
import {
|
|
28
|
-
ensurePrivateDirectorySync,
|
|
29
|
-
PRIVATE_FILE_MODE,
|
|
30
|
-
securePrivateFileSync,
|
|
31
|
-
} from "./privateDataPath";
|
|
32
|
-
|
|
33
|
-
export { ProviderConfigSchema };
|
|
34
|
-
export type { ProviderConfig };
|
|
35
|
-
|
|
36
|
-
const ProvidersStoreSchema = z.object({
|
|
37
|
-
providers: z.array(ProviderConfigSchema),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
type ProvidersStore = z.infer<typeof ProvidersStoreSchema>;
|
|
41
|
-
|
|
42
|
-
function normalizeMetadataModelName(value: string): string {
|
|
43
|
-
return value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function mergeModelMetadata(
|
|
47
|
-
existing: readonly ProviderModelMetadata[] | undefined,
|
|
48
|
-
incoming: readonly ProviderModelMetadata[] | undefined,
|
|
49
|
-
): ProviderModelMetadata[] | undefined {
|
|
50
|
-
if (existing === undefined && incoming === undefined) return undefined;
|
|
51
|
-
|
|
52
|
-
const merged = new Map<string, ProviderModelMetadata>();
|
|
53
|
-
for (const metadata of existing ?? []) {
|
|
54
|
-
merged.set(normalizeMetadataModelName(metadata.model), metadata);
|
|
55
|
-
}
|
|
56
|
-
for (const metadata of incoming ?? []) {
|
|
57
|
-
merged.set(normalizeMetadataModelName(metadata.model), metadata);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return [...merged.values()].toSorted((left, right) => left.model.localeCompare(right.model));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const LegacyProviderConfigSchema = z.object({
|
|
64
|
-
id: z.string().optional(),
|
|
65
|
-
name: z.string().optional(),
|
|
66
|
-
type: z.enum(["anthropic", "openai"]).optional(),
|
|
67
|
-
format: z.enum(["anthropic", "openai"]).optional(),
|
|
68
|
-
apiKey: z.string().optional(),
|
|
69
|
-
model: ProviderModelNameSchema.optional(),
|
|
70
|
-
models: ProviderModelsSchema.optional(),
|
|
71
|
-
baseUrl: z.string().optional(),
|
|
72
|
-
anthropicBaseUrl: z.string().optional(),
|
|
73
|
-
openaiBaseUrl: z.string().optional(),
|
|
74
|
-
openaiResponsesBaseUrl: z.string().optional(),
|
|
75
|
-
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
76
|
-
apiDocsUrl: z.string().optional(),
|
|
77
|
-
modelMetadataUrl: z.string().optional(),
|
|
78
|
-
modelMetadata: ProviderModelMetadataListSchema.optional(),
|
|
79
|
-
source: z.enum(["company", "personal"]).optional(),
|
|
80
|
-
createdAt: z.string().optional(),
|
|
81
|
-
updatedAt: z.string().optional(),
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
export type LegacyProviderConfig = z.infer<typeof LegacyProviderConfigSchema>;
|
|
85
|
-
|
|
86
|
-
const LegacyProviderStoreSchema = z.object({
|
|
87
|
-
providers: z.array(LegacyProviderConfigSchema),
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
function legacyModels(provider: LegacyProviderConfig): string[] {
|
|
91
|
-
const modelList = provider.models?.map((model) => model.trim()).filter((model) => model !== "");
|
|
92
|
-
if (modelList !== undefined && modelList.length > 0) return modelList;
|
|
93
|
-
const model = provider.model?.trim();
|
|
94
|
-
if (model !== undefined && model !== "") return [model];
|
|
95
|
-
return [""];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function migrateLegacyProvider(
|
|
99
|
-
provider: LegacyProviderConfig,
|
|
100
|
-
index: number,
|
|
101
|
-
now: string,
|
|
102
|
-
): ProviderConfig | null {
|
|
103
|
-
const apiKey = normalizeApiKey(provider.apiKey ?? "");
|
|
104
|
-
if (apiKey === "") return null;
|
|
105
|
-
|
|
106
|
-
const format = provider.format ?? provider.type;
|
|
107
|
-
const fallbackName =
|
|
108
|
-
format === "openai" ? "OpenAI" : format === "anthropic" ? "Anthropic" : "Legacy Provider";
|
|
109
|
-
const trimmedName = provider.name?.trim();
|
|
110
|
-
const name =
|
|
111
|
-
trimmedName !== undefined && trimmedName !== "" ? trimmedName : `${fallbackName} ${index + 1}`;
|
|
112
|
-
const baseUrl = provider.baseUrl;
|
|
113
|
-
const anthropicBaseUrl =
|
|
114
|
-
provider.anthropicBaseUrl ?? (format === "anthropic" && baseUrl !== undefined ? baseUrl : "");
|
|
115
|
-
const openaiBaseUrl =
|
|
116
|
-
provider.openaiBaseUrl ?? (format === "openai" && baseUrl !== undefined ? baseUrl : "");
|
|
117
|
-
const openaiResponsesBaseUrl = provider.openaiResponsesBaseUrl ?? "";
|
|
118
|
-
|
|
119
|
-
const candidate: ProviderConfig = {
|
|
120
|
-
id: provider.id ?? randomUUID(),
|
|
121
|
-
name,
|
|
122
|
-
apiKey,
|
|
123
|
-
model: provider.model,
|
|
124
|
-
models: legacyModels(provider),
|
|
125
|
-
format,
|
|
126
|
-
baseUrl,
|
|
127
|
-
anthropicBaseUrl,
|
|
128
|
-
openaiBaseUrl,
|
|
129
|
-
openaiResponsesBaseUrl,
|
|
130
|
-
authHeader: provider.authHeader ?? "bearer",
|
|
131
|
-
apiDocsUrl: provider.apiDocsUrl,
|
|
132
|
-
modelMetadataUrl: provider.modelMetadataUrl,
|
|
133
|
-
modelMetadata: provider.modelMetadata,
|
|
134
|
-
source: provider.source,
|
|
135
|
-
createdAt: provider.createdAt ?? now,
|
|
136
|
-
updatedAt: provider.updatedAt ?? now,
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const parsed = ProviderConfigSchema.safeParse(candidate);
|
|
140
|
-
return parsed.success ? withBuiltinProviderModelMetadata(parsed.data, now) : null;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function migrateFromDataDirConfig(targetStore: Conf<ProvidersStore>, dataDirPath: string): void {
|
|
144
|
-
if (process.env["AGENT_INSPECTOR_CONFIG_PATH"] !== undefined) return;
|
|
145
|
-
if (targetStore.get("providers", []).length > 0) return;
|
|
146
|
-
|
|
147
|
-
const legacyConfigPath = join(dataDirPath, "config.json");
|
|
148
|
-
if (!existsSync(legacyConfigPath)) return;
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const raw = readFileSync(legacyConfigPath, "utf-8");
|
|
152
|
-
const parsedJson: unknown = JSON.parse(raw);
|
|
153
|
-
const parsed = LegacyProviderStoreSchema.safeParse(parsedJson);
|
|
154
|
-
if (!parsed.success) return;
|
|
155
|
-
|
|
156
|
-
const now = new Date().toISOString();
|
|
157
|
-
const migrated: ProviderConfig[] = [];
|
|
158
|
-
for (let index = 0; index < parsed.data.providers.length; index++) {
|
|
159
|
-
const provider = parsed.data.providers[index];
|
|
160
|
-
if (provider === undefined) continue;
|
|
161
|
-
const current = migrateLegacyProvider(provider, index, now);
|
|
162
|
-
if (current !== null) migrated.push(current);
|
|
163
|
-
}
|
|
164
|
-
if (migrated.length === 0) return;
|
|
165
|
-
|
|
166
|
-
if (!persistProviders(migrated)) {
|
|
167
|
-
logger.warn(`[providers] Data-dir config migration left ${targetStore.path} unchanged`);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
logger.info(
|
|
171
|
-
`[providers] Migrated ${migrated.length} provider(s) from ${legacyConfigPath} to ${targetStore.path}`,
|
|
172
|
-
);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
logger.warn(`[providers] Data-dir config migration failed: ${String(err)}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* One-time migration from the legacy conf v15 default location
|
|
180
|
-
* (`%APPDATA%\agent-inspector-nodejs\Config\config.json` on Windows,
|
|
181
|
-
* `$XDG_CONFIG_HOME/agent-inspector-nodejs/Config/config.json` on Linux).
|
|
182
|
-
* Older versions of agent-inspector let the `conf` library pick the path,
|
|
183
|
-
* which produced a project-suffixed directory under the OS user-config
|
|
184
|
-
* root. Users who upgrade now have their providers copied to the new
|
|
185
|
-
* `<dataDir>/providers.json` location; the legacy file is left in
|
|
186
|
-
* place for safety and can be deleted manually.
|
|
187
|
-
*
|
|
188
|
-
* The check is "new store is still empty" rather than "new file doesn't
|
|
189
|
-
* exist" because `Conf` materialises the defaults file on construction
|
|
190
|
-
* — so the file is always present immediately after the `new Conf()`
|
|
191
|
-
* call above, even on a brand-new install.
|
|
192
|
-
*/
|
|
193
|
-
function migrateFromLegacyConfLocation(targetStore: Conf<ProvidersStore>): void {
|
|
194
|
-
if (process.env["AGENT_INSPECTOR_CONFIG_PATH"] !== undefined) return;
|
|
195
|
-
if (hasExplicitDataDir()) return;
|
|
196
|
-
if (targetStore.get("providers", []).length > 0) return;
|
|
197
|
-
let legacyStore: Conf<ProvidersStore> | undefined;
|
|
198
|
-
try {
|
|
199
|
-
legacyStore = new Conf<ProvidersStore>({
|
|
200
|
-
projectName: "agent-inspector",
|
|
201
|
-
defaults: { providers: [] },
|
|
202
|
-
});
|
|
203
|
-
const legacyProviders = legacyStore.get("providers", []);
|
|
204
|
-
if (legacyProviders.length === 0) return;
|
|
205
|
-
const now = new Date().toISOString();
|
|
206
|
-
const migrated = legacyProviders.map((provider) =>
|
|
207
|
-
withBuiltinProviderModelMetadata(provider, now),
|
|
208
|
-
);
|
|
209
|
-
if (!persistProviders(migrated)) {
|
|
210
|
-
logger.warn(`[providers] Legacy migration left ${targetStore.path} unchanged`);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
logger.info(
|
|
214
|
-
`[providers] Migrated ${legacyProviders.length} provider(s) from ${legacyStore.path} to ${targetStore.path}`,
|
|
215
|
-
);
|
|
216
|
-
} catch (err) {
|
|
217
|
-
logger.warn(`[providers] Legacy migration failed: ${String(err)}`);
|
|
218
|
-
} finally {
|
|
219
|
-
legacyStore?._closeWatcher();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Provider metadata uses Conf; API keys are stored separately by ProviderSecretStore.
|
|
224
|
-
const configPath = process.env["AGENT_INSPECTOR_CONFIG_PATH"];
|
|
225
|
-
|
|
226
|
-
// The data directory is shared with the runtime config (`config.ts`).
|
|
227
|
-
// We pass `cwd` so conf writes the file as `<cwd>/<configName>.<ext>`,
|
|
228
|
-
// i.e. `<dataDir>/providers.json`. The runtime config lives at
|
|
229
|
-
// `<dataDir>/config.json`; the different filenames keep the two stores
|
|
230
|
-
// from overwriting each other.
|
|
231
|
-
const dataDir = getDataDir();
|
|
232
|
-
const providerDataDir = configPath === undefined ? dataDir : dirname(configPath);
|
|
233
|
-
if (!ensurePrivateDirectorySync(providerDataDir)) {
|
|
234
|
-
logger.error(`[providers] Could not secure provider data directory ${providerDataDir}`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export const store = new Conf<ProvidersStore>({
|
|
238
|
-
cwd: dataDir,
|
|
239
|
-
configName: "providers",
|
|
240
|
-
defaults: {
|
|
241
|
-
providers: [],
|
|
242
|
-
},
|
|
243
|
-
...(configPath !== undefined ? { path: configPath } : {}),
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
const providerSecretStore: ProviderSecretStore = createProviderSecretStore(dirname(store.path));
|
|
247
|
-
|
|
248
|
-
function providerMetadataStorageIsPrivate(forceFile = false): boolean {
|
|
249
|
-
if (!ensurePrivateDirectorySync(dirname(store.path))) return false;
|
|
250
|
-
if (!existsSync(store.path)) return true;
|
|
251
|
-
return securePrivateFileSync(store.path, forceFile);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function writeProviderMetadata(providers: readonly ProviderConfig[]): boolean {
|
|
255
|
-
if (!providerMetadataStorageIsPrivate()) {
|
|
256
|
-
logger.error(`[providers] Refusing provider metadata write to unsafe path ${store.path}`);
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
store.set("providers", [...providers]);
|
|
261
|
-
} catch (error) {
|
|
262
|
-
logger.error(`[providers] Could not persist provider metadata: ${String(error)}`);
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
if (securePrivateFileSync(store.path, true)) return true;
|
|
266
|
-
logger.error(`[providers] Could not secure provider metadata at ${store.path}`);
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (!providerMetadataStorageIsPrivate(true)) {
|
|
271
|
-
logger.error(`[providers] Provider metadata path is not private: ${store.path}`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function providerWithResolvedSecret(provider: ProviderConfig): ProviderConfig | null {
|
|
275
|
-
if (!providerSecretStore.isReference(provider.apiKey)) {
|
|
276
|
-
logger.error(
|
|
277
|
-
`[providers] Refusing plaintext secret for provider ${provider.id}; secure migration is required`,
|
|
278
|
-
);
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
const apiKey = providerSecretStore.read(provider.apiKey);
|
|
282
|
-
if (apiKey !== null) return { ...provider, apiKey };
|
|
283
|
-
logger.error(`[providers] Secret is missing or unreadable for provider "${provider.name}"`);
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function persistProviders(
|
|
288
|
-
providers: readonly ProviderConfig[],
|
|
289
|
-
removedProviderIds: ReadonlySet<string> = new Set(),
|
|
290
|
-
): boolean {
|
|
291
|
-
const existingReferences = new Map<string, string>();
|
|
292
|
-
const unresolvedRecords = new Map<string, ProviderConfig>();
|
|
293
|
-
for (const candidate of store.get("providers", [])) {
|
|
294
|
-
const parsed = ProviderConfigSchema.safeParse(candidate);
|
|
295
|
-
if (parsed.success && providerSecretStore.isReference(parsed.data.apiKey)) {
|
|
296
|
-
existingReferences.set(parsed.data.id, parsed.data.apiKey);
|
|
297
|
-
if (providerSecretStore.read(parsed.data.apiKey) === null) {
|
|
298
|
-
unresolvedRecords.set(parsed.data.id, parsed.data);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const persisted: ProviderConfig[] = [];
|
|
304
|
-
for (const provider of providers) {
|
|
305
|
-
const existingReference = existingReferences.get(provider.id);
|
|
306
|
-
if (existingReference !== undefined) {
|
|
307
|
-
const existingSecret = providerSecretStore.read(existingReference);
|
|
308
|
-
if (existingSecret !== null && secretValuesEqual(existingSecret, provider.apiKey)) {
|
|
309
|
-
persisted.push({ ...provider, apiKey: existingReference });
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const reference = providerSecretStore.write(provider.id, provider.apiKey);
|
|
315
|
-
if (reference !== null) {
|
|
316
|
-
persisted.push({ ...provider, apiKey: reference });
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
logger.error(
|
|
320
|
-
`[providers] Secret store unavailable; refusing to persist provider ${provider.id}`,
|
|
321
|
-
);
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
const persistedIds = new Set(persisted.map((provider) => provider.id));
|
|
325
|
-
for (const [id, provider] of unresolvedRecords) {
|
|
326
|
-
if (!persistedIds.has(id) && !removedProviderIds.has(id)) persisted.push(provider);
|
|
327
|
-
}
|
|
328
|
-
return writeProviderMetadata(persisted);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function migratePlaintextProviderSecrets(): boolean {
|
|
332
|
-
const raw = store.get("providers", []);
|
|
333
|
-
let changed = false;
|
|
334
|
-
const migrated: ProviderConfig[] = [];
|
|
335
|
-
for (const candidate of raw) {
|
|
336
|
-
const parsed = ProviderConfigSchema.safeParse(candidate);
|
|
337
|
-
if (!parsed.success) {
|
|
338
|
-
migrated.push(candidate);
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
if (providerSecretStore.isReference(parsed.data.apiKey)) {
|
|
342
|
-
migrated.push(parsed.data);
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
const reference = providerSecretStore.write(parsed.data.id, parsed.data.apiKey);
|
|
346
|
-
if (reference === null) {
|
|
347
|
-
logger.error(
|
|
348
|
-
`[providers] Could not migrate provider ${parsed.data.id} to secure storage; leaving the existing file unchanged`,
|
|
349
|
-
);
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
changed = true;
|
|
353
|
-
migrated.push({ ...parsed.data, apiKey: reference });
|
|
354
|
-
}
|
|
355
|
-
if (!changed) return true;
|
|
356
|
-
if (writeProviderMetadata(migrated)) return true;
|
|
357
|
-
logger.error("[providers] Could not finish provider secret migration");
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function sanitizeProviderBackupValue(value: unknown): unknown {
|
|
362
|
-
if (Array.isArray(value)) return value.map(sanitizeProviderBackupValue);
|
|
363
|
-
if (value === null || typeof value !== "object") return value;
|
|
364
|
-
|
|
365
|
-
return Object.fromEntries(
|
|
366
|
-
Object.entries(value).map(([key, entry]) => {
|
|
367
|
-
if (key === "apiKey" && typeof entry === "string") {
|
|
368
|
-
return [key, "redacted"];
|
|
369
|
-
}
|
|
370
|
-
return [key, sanitizeProviderBackupValue(entry)];
|
|
371
|
-
}),
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function sanitizeProviderBackup(): void {
|
|
376
|
-
const backupPath = `${store.path}.bak`;
|
|
377
|
-
if (!existsSync(backupPath)) return;
|
|
378
|
-
|
|
379
|
-
const temporaryPath = `${backupPath}.${String(process.pid)}.tmp`;
|
|
380
|
-
try {
|
|
381
|
-
const parsed: unknown = JSON.parse(readFileSync(backupPath, "utf-8"));
|
|
382
|
-
const sanitized = sanitizeProviderBackupValue(parsed);
|
|
383
|
-
writeFileSync(temporaryPath, JSON.stringify(sanitized, null, 2), {
|
|
384
|
-
encoding: "utf8",
|
|
385
|
-
mode: PRIVATE_FILE_MODE,
|
|
386
|
-
});
|
|
387
|
-
if (!securePrivateFileSync(temporaryPath, true)) {
|
|
388
|
-
rmSync(temporaryPath, { force: true });
|
|
389
|
-
rmSync(backupPath, { force: true });
|
|
390
|
-
logger.error("[providers] Removed provider backup after temporary permission failure");
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
renameSync(temporaryPath, backupPath);
|
|
394
|
-
if (!securePrivateFileSync(backupPath, true)) {
|
|
395
|
-
rmSync(backupPath, { force: true });
|
|
396
|
-
logger.error("[providers] Removed provider backup after permission failure");
|
|
397
|
-
}
|
|
398
|
-
} catch (error) {
|
|
399
|
-
rmSync(temporaryPath, { force: true });
|
|
400
|
-
rmSync(backupPath, { force: true });
|
|
401
|
-
logger.error(
|
|
402
|
-
`[providers] Removed provider backup that could not be sanitized: ${String(error)}`,
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Older builds stored providers in `<dataDir>/config.json`. Migrate that
|
|
408
|
-
// shape before checking the older Conf-managed default location.
|
|
409
|
-
migrateFromDataDirConfig(store, dataDir);
|
|
410
|
-
|
|
411
|
-
// Run the legacy migration once on module load. No-op when the new file
|
|
412
|
-
// already has data, when the user has set AGENT_INSPECTOR_CONFIG_PATH or
|
|
413
|
-
// an explicit data dir, or when the legacy file is empty.
|
|
414
|
-
migrateFromLegacyConfLocation(store);
|
|
415
|
-
sanitizeProviderBackup();
|
|
416
|
-
const providerSecretsReady = migratePlaintextProviderSecrets();
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Expands the old `{ format, baseUrl }` representation into the matching
|
|
420
|
-
* format-specific URL. Once either format-specific field exists, the record is
|
|
421
|
-
* already on the current schema and must be preserved exactly: an empty or
|
|
422
|
-
* missing counterpart means that API format was intentionally not configured.
|
|
423
|
-
*/
|
|
424
|
-
export function migrateProvider(p: ProviderConfig): ProviderConfig {
|
|
425
|
-
if (
|
|
426
|
-
p.anthropicBaseUrl !== undefined ||
|
|
427
|
-
p.openaiBaseUrl !== undefined ||
|
|
428
|
-
p.openaiResponsesBaseUrl !== undefined
|
|
429
|
-
) {
|
|
430
|
-
return p;
|
|
431
|
-
}
|
|
432
|
-
if (p.format === undefined || p.baseUrl === undefined || p.baseUrl === "") {
|
|
433
|
-
return p;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return p.format === "openai"
|
|
437
|
-
? { ...p, anthropicBaseUrl: "", openaiBaseUrl: p.baseUrl, openaiResponsesBaseUrl: "" }
|
|
438
|
-
: { ...p, anthropicBaseUrl: p.baseUrl, openaiBaseUrl: "", openaiResponsesBaseUrl: "" };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Migrates existing provider configs to preserve Anthropic and OpenAI routing URLs.
|
|
443
|
-
*/
|
|
444
|
-
function migrateProviders(): void {
|
|
445
|
-
const providers = getProviders();
|
|
446
|
-
const updated = providers.map(migrateProvider);
|
|
447
|
-
const migrated = updated.some((p, i) => JSON.stringify(p) !== JSON.stringify(providers[i]));
|
|
448
|
-
|
|
449
|
-
if (migrated) {
|
|
450
|
-
void persistProviders(updated);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Run migration on module load
|
|
455
|
-
if (providerSecretsReady) migrateProviders();
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Migrate: merge providers with identical API key and routing URLs.
|
|
459
|
-
* into multi-model providers. Existing single-model providers get their `model`
|
|
460
|
-
* value promoted to `models: [model]`.
|
|
461
|
-
*/
|
|
462
|
-
function migrateMultiModel(): void {
|
|
463
|
-
const providers = getProviders();
|
|
464
|
-
if (providers.length === 0) return;
|
|
465
|
-
let changed = false;
|
|
466
|
-
|
|
467
|
-
// Step 1: Promote single model to models array for any provider missing models
|
|
468
|
-
const promoted = providers.map((p) => {
|
|
469
|
-
if (p.models !== undefined && p.models.length > 0) return p;
|
|
470
|
-
const models = p.model !== undefined && p.model !== "" ? [p.model] : [];
|
|
471
|
-
changed = true;
|
|
472
|
-
return { ...p, models };
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
// Step 2: Merge providers with the same API key and routing URLs.
|
|
476
|
-
const groups = new Map<string, ProviderConfig[]>();
|
|
477
|
-
for (const p of promoted) {
|
|
478
|
-
const key = `${p.apiKey}::${p.anthropicBaseUrl ?? ""}::${p.openaiBaseUrl ?? ""}::${p.openaiResponsesBaseUrl ?? ""}`;
|
|
479
|
-
const group = groups.get(key);
|
|
480
|
-
if (group !== undefined) {
|
|
481
|
-
group.push(p);
|
|
482
|
-
} else {
|
|
483
|
-
groups.set(key, [p]);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const merged: ProviderConfig[] = [];
|
|
488
|
-
for (const group of groups.values()) {
|
|
489
|
-
const first = group[0];
|
|
490
|
-
if (group.length === 1 && first !== undefined) {
|
|
491
|
-
merged.push(first);
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
changed = true;
|
|
495
|
-
// Merge: keep the earliest provider's metadata, combine all models (deduplicated)
|
|
496
|
-
const sorted = group.toSorted(
|
|
497
|
-
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
|
498
|
-
);
|
|
499
|
-
const earliest = sorted[0];
|
|
500
|
-
if (earliest === undefined) continue;
|
|
501
|
-
const allModels = new Set<string>();
|
|
502
|
-
for (const p of sorted) {
|
|
503
|
-
for (const m of p.models ?? []) {
|
|
504
|
-
if (m !== "") allModels.add(m);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
// Take first non-empty source
|
|
508
|
-
const mergedSource = sorted.find((p) => p.source !== undefined)?.source;
|
|
509
|
-
merged.push({
|
|
510
|
-
...earliest,
|
|
511
|
-
models: [...allModels],
|
|
512
|
-
source: mergedSource,
|
|
513
|
-
updatedAt: new Date().toISOString(),
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (changed && merged.length > 0) {
|
|
518
|
-
// Safety backup before writing
|
|
519
|
-
try {
|
|
520
|
-
writeFileSync(`${store.path}.bak`, readFileSync(store.path, "utf-8"), {
|
|
521
|
-
mode: PRIVATE_FILE_MODE,
|
|
522
|
-
});
|
|
523
|
-
sanitizeProviderBackup();
|
|
524
|
-
} catch {
|
|
525
|
-
// best-effort, ignore
|
|
526
|
-
}
|
|
527
|
-
void persistProviders(merged);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (providerSecretsReady) migrateMultiModel();
|
|
532
|
-
|
|
533
|
-
function migrateBuiltinModelMetadata(): void {
|
|
534
|
-
const providers = getProviders();
|
|
535
|
-
if (providers.length === 0) return;
|
|
536
|
-
|
|
537
|
-
const now = new Date().toISOString();
|
|
538
|
-
const updated = providers.map((provider) => withBuiltinProviderModelMetadata(provider, now));
|
|
539
|
-
const changed = updated.some(
|
|
540
|
-
(provider, index) => JSON.stringify(provider) !== JSON.stringify(providers[index]),
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
if (changed) {
|
|
544
|
-
void persistProviders(updated);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (providerSecretsReady) migrateBuiltinModelMetadata();
|
|
549
|
-
|
|
550
|
-
// Override with JSON env var if provided (for testing)
|
|
551
|
-
const providersJson = process.env["AGENT_INSPECTOR_PROVIDERS_JSON"];
|
|
552
|
-
if (providersJson !== undefined) {
|
|
553
|
-
try {
|
|
554
|
-
const parsed = ProviderConfigSchema.array().safeParse(JSON.parse(providersJson));
|
|
555
|
-
if (parsed.success) {
|
|
556
|
-
// Apply migration to JSON providers (e.g., convert baseUrl to openaiBaseUrl/anthropicBaseUrl)
|
|
557
|
-
const now = new Date().toISOString();
|
|
558
|
-
const migrated = parsed.data
|
|
559
|
-
.map(migrateProvider)
|
|
560
|
-
.map((provider) => withBuiltinProviderModelMetadata(provider, now));
|
|
561
|
-
void persistProviders(migrated);
|
|
562
|
-
}
|
|
563
|
-
} catch {
|
|
564
|
-
// Ignore invalid JSON
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
export function getProviders(): ProviderConfig[] {
|
|
569
|
-
const raw = store.get("providers", []);
|
|
570
|
-
// Validate the on-disk store entry-by-entry so a single corrupted record
|
|
571
|
-
// cannot wipe out the entire provider list. The whole-store safeParse was
|
|
572
|
-
// too strict — once any legacy/in-flight provider failed validation the
|
|
573
|
-
// whole list was discarded, which broke `updateProvider` and routing.
|
|
574
|
-
const valid: ProviderConfig[] = [];
|
|
575
|
-
for (const candidate of raw) {
|
|
576
|
-
const entry = ProviderConfigSchema.safeParse(candidate);
|
|
577
|
-
if (entry.success) {
|
|
578
|
-
const resolved = providerWithResolvedSecret(entry.data);
|
|
579
|
-
if (resolved !== null) valid.push(resolved);
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
let candidateName = "<unknown>";
|
|
583
|
-
if (typeof candidate === "object" && candidate !== null) {
|
|
584
|
-
const nameResult = z.object({ name: z.string() }).safeParse(candidate);
|
|
585
|
-
if (nameResult.success) candidateName = nameResult.data.name;
|
|
586
|
-
}
|
|
587
|
-
logger.warn(
|
|
588
|
-
`[providers] Skipping invalid on-disk provider "${candidateName}": ${entry.error.message}`,
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
return valid;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
export function getProvider(id: string): ProviderConfig | undefined {
|
|
595
|
-
const providers = getProviders();
|
|
596
|
-
return providers.find((p) => p.id === id);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
export function redactProvider(provider: ProviderConfig): RedactedProviderDto {
|
|
600
|
-
return toRedactedProviderDto(provider);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export function redactProviders(providers: readonly ProviderConfig[]): RedactedProviderDto[] {
|
|
604
|
-
return toRedactedProviderDtos(providers);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Normalizes an API key by stripping "Bearer " prefix if present.
|
|
609
|
-
*/
|
|
610
|
-
export function normalizeApiKey(apiKey: string): string {
|
|
611
|
-
return apiKey.replace(/^Bearer\s+/i, "").trim();
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function providerUpdateApiKey(candidate: string | undefined, existingApiKey: string): string {
|
|
615
|
-
if (candidate === undefined) return existingApiKey;
|
|
616
|
-
const normalized = normalizeApiKey(candidate);
|
|
617
|
-
return normalized === REDACTED_PROVIDER_API_KEY ? existingApiKey : normalized;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
export function addProvider(
|
|
621
|
-
name: string,
|
|
622
|
-
apiKey: string,
|
|
623
|
-
format?: "anthropic" | "openai",
|
|
624
|
-
baseUrl?: string,
|
|
625
|
-
models?: string[],
|
|
626
|
-
authHeader?: "bearer" | "x-api-key",
|
|
627
|
-
apiDocsUrl?: string,
|
|
628
|
-
anthropicBaseUrl?: string,
|
|
629
|
-
openaiBaseUrl?: string,
|
|
630
|
-
source?: "company" | "personal",
|
|
631
|
-
modelMetadataUrl?: string,
|
|
632
|
-
modelMetadata?: ProviderModelMetadata[],
|
|
633
|
-
openaiResponsesBaseUrl?: string,
|
|
634
|
-
): ProviderConfig | null {
|
|
635
|
-
const providers = getProviders();
|
|
636
|
-
const normalizedKey = normalizeApiKey(apiKey);
|
|
637
|
-
const resolvedAnthropicUrl =
|
|
638
|
-
anthropicBaseUrl ?? (format === "anthropic" && baseUrl !== undefined ? baseUrl : "");
|
|
639
|
-
const resolvedOpenaiUrl =
|
|
640
|
-
openaiBaseUrl ?? (format === "openai" && baseUrl !== undefined ? baseUrl : "");
|
|
641
|
-
const resolvedOpenaiResponsesUrl = openaiResponsesBaseUrl ?? "";
|
|
642
|
-
const now = new Date().toISOString();
|
|
643
|
-
|
|
644
|
-
// If a provider with the same API key and routing URLs already exists,
|
|
645
|
-
// merge the new models into it instead of creating a duplicate.
|
|
646
|
-
const existing = providers.find(
|
|
647
|
-
(p) =>
|
|
648
|
-
secretValuesEqual(p.apiKey, normalizedKey) &&
|
|
649
|
-
(p.anthropicBaseUrl ?? "") === (resolvedAnthropicUrl ?? "") &&
|
|
650
|
-
(p.openaiBaseUrl ?? "") === (resolvedOpenaiUrl ?? "") &&
|
|
651
|
-
(p.openaiResponsesBaseUrl ?? "") === (resolvedOpenaiResponsesUrl ?? ""),
|
|
652
|
-
);
|
|
653
|
-
if (existing) {
|
|
654
|
-
const existingIndex = providers.findIndex((p) => p.id === existing.id);
|
|
655
|
-
const newModels = (models ?? []).filter((m) => m !== "");
|
|
656
|
-
existing.modelMetadataUrl = modelMetadataUrl ?? existing.modelMetadataUrl;
|
|
657
|
-
existing.modelMetadata = mergeModelMetadata(existing.modelMetadata, modelMetadata);
|
|
658
|
-
if (newModels.length > 0) {
|
|
659
|
-
const mergedModels = new Set(existing.models ?? []);
|
|
660
|
-
for (const m of newModels) mergedModels.add(m);
|
|
661
|
-
existing.models = [...mergedModels];
|
|
662
|
-
}
|
|
663
|
-
existing.updatedAt = now;
|
|
664
|
-
const enriched = withBuiltinProviderModelMetadata(existing, now);
|
|
665
|
-
if (existingIndex >= 0) {
|
|
666
|
-
providers[existingIndex] = enriched;
|
|
667
|
-
}
|
|
668
|
-
if (!persistProviders(providers)) return null;
|
|
669
|
-
return enriched;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const newProvider: ProviderConfig = withBuiltinProviderModelMetadata(
|
|
673
|
-
{
|
|
674
|
-
id: randomUUID(),
|
|
675
|
-
name,
|
|
676
|
-
apiKey: normalizedKey,
|
|
677
|
-
format:
|
|
678
|
-
format ??
|
|
679
|
-
(anthropicBaseUrl !== undefined
|
|
680
|
-
? "anthropic"
|
|
681
|
-
: openaiBaseUrl !== undefined || openaiResponsesBaseUrl !== undefined
|
|
682
|
-
? "openai"
|
|
683
|
-
: undefined),
|
|
684
|
-
baseUrl,
|
|
685
|
-
models: models ?? [],
|
|
686
|
-
authHeader: authHeader ?? "bearer",
|
|
687
|
-
apiDocsUrl,
|
|
688
|
-
modelMetadataUrl,
|
|
689
|
-
modelMetadata,
|
|
690
|
-
createdAt: now,
|
|
691
|
-
updatedAt: now,
|
|
692
|
-
anthropicBaseUrl: resolvedAnthropicUrl,
|
|
693
|
-
openaiBaseUrl: resolvedOpenaiUrl,
|
|
694
|
-
openaiResponsesBaseUrl: resolvedOpenaiResponsesUrl,
|
|
695
|
-
source,
|
|
696
|
-
},
|
|
697
|
-
now,
|
|
698
|
-
);
|
|
699
|
-
providers.push(newProvider);
|
|
700
|
-
if (!persistProviders(providers)) return null;
|
|
701
|
-
return newProvider;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
export function updateProvider(
|
|
705
|
-
id: string,
|
|
706
|
-
updates: Partial<Omit<ProviderConfig, "id" | "createdAt">>,
|
|
707
|
-
): ProviderConfig | null {
|
|
708
|
-
const providers = getProviders();
|
|
709
|
-
const existing = providers.find((p) => p.id === id);
|
|
710
|
-
if (!existing) return null;
|
|
711
|
-
|
|
712
|
-
const updatedAt = new Date().toISOString();
|
|
713
|
-
const updated: ProviderConfig = withBuiltinProviderModelMetadata(
|
|
714
|
-
{
|
|
715
|
-
id: existing.id,
|
|
716
|
-
name: updates.name ?? existing.name,
|
|
717
|
-
apiKey: providerUpdateApiKey(updates.apiKey, existing.apiKey),
|
|
718
|
-
models: updates.models !== undefined ? updates.models : existing.models,
|
|
719
|
-
format: updates.format ?? existing.format,
|
|
720
|
-
baseUrl: updates.baseUrl !== undefined ? updates.baseUrl : existing.baseUrl,
|
|
721
|
-
authHeader: updates.authHeader ?? existing.authHeader,
|
|
722
|
-
apiDocsUrl: updates.apiDocsUrl ?? existing.apiDocsUrl,
|
|
723
|
-
modelMetadataUrl:
|
|
724
|
-
updates.modelMetadataUrl !== undefined
|
|
725
|
-
? updates.modelMetadataUrl
|
|
726
|
-
: existing.modelMetadataUrl,
|
|
727
|
-
modelMetadata:
|
|
728
|
-
updates.modelMetadata !== undefined ? updates.modelMetadata : existing.modelMetadata,
|
|
729
|
-
createdAt: existing.createdAt,
|
|
730
|
-
updatedAt,
|
|
731
|
-
// Handle format-specific URLs
|
|
732
|
-
anthropicBaseUrl:
|
|
733
|
-
updates.anthropicBaseUrl !== undefined
|
|
734
|
-
? updates.anthropicBaseUrl
|
|
735
|
-
: existing.anthropicBaseUrl,
|
|
736
|
-
openaiBaseUrl:
|
|
737
|
-
updates.openaiBaseUrl !== undefined ? updates.openaiBaseUrl : existing.openaiBaseUrl,
|
|
738
|
-
openaiResponsesBaseUrl:
|
|
739
|
-
updates.openaiResponsesBaseUrl !== undefined
|
|
740
|
-
? updates.openaiResponsesBaseUrl
|
|
741
|
-
: existing.openaiResponsesBaseUrl,
|
|
742
|
-
source: updates.source !== undefined ? updates.source : existing.source,
|
|
743
|
-
},
|
|
744
|
-
updatedAt,
|
|
745
|
-
);
|
|
746
|
-
const index = providers.findIndex((p) => p.id === id);
|
|
747
|
-
providers[index] = updated;
|
|
748
|
-
if (!persistProviders(providers)) return null;
|
|
749
|
-
return updated;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
export function deleteProvider(id: string): boolean {
|
|
753
|
-
const providers = getProviders();
|
|
754
|
-
const filtered = providers.filter((p) => p.id !== id);
|
|
755
|
-
if (filtered.length === providers.length) return false;
|
|
756
|
-
|
|
757
|
-
if (!persistProviders(filtered, new Set([id]))) return false;
|
|
758
|
-
providerSecretStore.delete(id);
|
|
759
|
-
return true;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* Export all providers as a JSON string (without API keys for security).
|
|
764
|
-
*/
|
|
765
|
-
export function exportProviders(): string {
|
|
766
|
-
const providers = getProviders();
|
|
767
|
-
// Mask API keys for security
|
|
768
|
-
const safeProviders = providers.map(({ apiKey, ...rest }) => ({
|
|
769
|
-
...rest,
|
|
770
|
-
apiKey: maskApiKey(apiKey),
|
|
771
|
-
}));
|
|
772
|
-
return JSON.stringify(safeProviders, null, 2);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Export all providers as a JSON string including API keys (user must opt-in).
|
|
777
|
-
*/
|
|
778
|
-
export function exportProvidersWithKeys(): string {
|
|
779
|
-
return JSON.stringify(getProviders(), null, 2);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Import providers from a JSON string.
|
|
784
|
-
* Returns { imported: number, errors: string[] }
|
|
785
|
-
*/
|
|
786
|
-
export function importProviders(json: string): { imported: number; errors: string[] } {
|
|
787
|
-
const errors: string[] = [];
|
|
788
|
-
let imported = 0;
|
|
789
|
-
|
|
790
|
-
try {
|
|
791
|
-
const parsed: unknown = JSON.parse(json);
|
|
792
|
-
|
|
793
|
-
// Use Zod to validate the structure
|
|
794
|
-
const ImportSchema = z.union([
|
|
795
|
-
z.array(ProviderConfigSchema),
|
|
796
|
-
z.object({
|
|
797
|
-
providers: z.array(ProviderConfigSchema),
|
|
798
|
-
}),
|
|
799
|
-
]);
|
|
800
|
-
|
|
801
|
-
const parseResult = ImportSchema.safeParse(parsed);
|
|
802
|
-
if (!parseResult.success) {
|
|
803
|
-
return {
|
|
804
|
-
imported: 0,
|
|
805
|
-
errors: [`Invalid format: ${parseResult.error.message}`],
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const providerArray = Array.isArray(parseResult.data)
|
|
810
|
-
? parseResult.data
|
|
811
|
-
: parseResult.data.providers;
|
|
812
|
-
|
|
813
|
-
// Snapshot existing providers once
|
|
814
|
-
const existingProviders = getProviders();
|
|
815
|
-
|
|
816
|
-
// Build a Set of existing (name, anthropicBaseUrl) keys for O(1) duplicate lookup
|
|
817
|
-
const existingKeys = new Set<string>();
|
|
818
|
-
for (const p of existingProviders) {
|
|
819
|
-
existingKeys.add(`${p.name}::${p.anthropicBaseUrl ?? ""}`);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const newProviders: ProviderConfig[] = [];
|
|
823
|
-
|
|
824
|
-
for (let i = 0; i < providerArray.length; i++) {
|
|
825
|
-
const item = providerArray[i];
|
|
826
|
-
const result = ProviderConfigSchema.safeParse(item);
|
|
827
|
-
|
|
828
|
-
if (!result.success) {
|
|
829
|
-
errors.push(`Provider ${i + 1}: ${result.error.message}`);
|
|
830
|
-
continue;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const provider = result.data;
|
|
834
|
-
const key = `${provider.name}::${provider.anthropicBaseUrl ?? ""}`;
|
|
835
|
-
|
|
836
|
-
if (existingKeys.has(key)) {
|
|
837
|
-
errors.push(`Provider "${provider.name}" already exists, skipping`);
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Generate new ID and add
|
|
842
|
-
const now = new Date().toISOString();
|
|
843
|
-
const newProvider: ProviderConfig = withBuiltinProviderModelMetadata(
|
|
844
|
-
{
|
|
845
|
-
...provider,
|
|
846
|
-
id: randomUUID(),
|
|
847
|
-
createdAt: now,
|
|
848
|
-
updatedAt: now,
|
|
849
|
-
},
|
|
850
|
-
now,
|
|
851
|
-
);
|
|
852
|
-
|
|
853
|
-
newProviders.push(newProvider);
|
|
854
|
-
existingKeys.add(key);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
// Single write: append all new providers at once
|
|
858
|
-
if (newProviders.length > 0) {
|
|
859
|
-
if (!persistProviders([...existingProviders, ...newProviders])) {
|
|
860
|
-
return {
|
|
861
|
-
imported: 0,
|
|
862
|
-
errors: ["Secure provider storage is unavailable; no providers were imported"],
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
imported = newProviders.length;
|
|
866
|
-
}
|
|
867
|
-
} catch (err) {
|
|
868
|
-
return {
|
|
869
|
-
imported: 0,
|
|
870
|
-
errors: [`Failed to parse JSON: ${err instanceof Error ? err.message : String(err)}`],
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
return { imported, errors };
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/**
|
|
878
|
-
* Converts display model name to API usage name based on provider-specific rules.
|
|
879
|
-
* Default behavior: returns model as-is.
|
|
880
|
-
* MiniMax: replaces spaces with hyphens.
|
|
881
|
-
*/
|
|
882
|
-
export function getModelUsageName(model: string, providerName?: string): string {
|
|
883
|
-
if (
|
|
884
|
-
providerName !== undefined &&
|
|
885
|
-
providerName !== "" &&
|
|
886
|
-
providerName.toLowerCase().includes("minimax")
|
|
887
|
-
) {
|
|
888
|
-
return model.replace(/ /g, "-");
|
|
889
|
-
}
|
|
890
|
-
return model;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
/**
|
|
894
|
-
* Normalize a model name for comparison: lowercase, replace whitespace with hyphens.
|
|
895
|
-
*/
|
|
896
|
-
function normalizeModelName(name: string): string {
|
|
897
|
-
return name.toLowerCase().replace(/\s+/g, "-");
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const ZHIPU_MODEL_PREFIXES = ["glm-", "charglm-", "cogview-", "embedding-", "rerank-"];
|
|
901
|
-
const ZHIPU_PROVIDER_ALIASES = ["zhipu", "zhipuai", "bigmodel", "zai", "glm"];
|
|
902
|
-
const ZHIPU_BASE_URL_MARKERS = [
|
|
903
|
-
"open.bigmodel.cn/api/paas/v4",
|
|
904
|
-
"open.bigmodel.cn/api/coding/paas/v4",
|
|
905
|
-
"open.bigmodel.cn/api/anthropic",
|
|
906
|
-
];
|
|
907
|
-
|
|
908
|
-
function normalizeProviderAlias(name: string): string {
|
|
909
|
-
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function providerRoutingUrls(provider: ProviderConfig): string[] {
|
|
913
|
-
const urls: string[] = [];
|
|
914
|
-
if (provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl.trim() !== "") {
|
|
915
|
-
urls.push(provider.anthropicBaseUrl);
|
|
916
|
-
}
|
|
917
|
-
if (provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl.trim() !== "") {
|
|
918
|
-
urls.push(provider.openaiBaseUrl);
|
|
919
|
-
}
|
|
920
|
-
if (
|
|
921
|
-
provider.openaiResponsesBaseUrl !== undefined &&
|
|
922
|
-
provider.openaiResponsesBaseUrl.trim() !== ""
|
|
923
|
-
) {
|
|
924
|
-
urls.push(provider.openaiResponsesBaseUrl);
|
|
925
|
-
}
|
|
926
|
-
if (provider.baseUrl !== undefined && provider.baseUrl.trim() !== "") {
|
|
927
|
-
urls.push(provider.baseUrl);
|
|
928
|
-
}
|
|
929
|
-
return urls;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function isZhipuProvider(provider: ProviderConfig): boolean {
|
|
933
|
-
const normalizedName = normalizeProviderAlias(provider.name);
|
|
934
|
-
const nameMatches = ZHIPU_PROVIDER_ALIASES.some((alias) => {
|
|
935
|
-
const normalizedAlias = normalizeProviderAlias(alias);
|
|
936
|
-
return normalizedName.includes(normalizedAlias) || normalizedAlias.includes(normalizedName);
|
|
937
|
-
});
|
|
938
|
-
if (nameMatches) return true;
|
|
939
|
-
|
|
940
|
-
return providerRoutingUrls(provider).some((url) => {
|
|
941
|
-
const normalizedUrl = url.toLowerCase().replace(/\/+$/, "");
|
|
942
|
-
return ZHIPU_BASE_URL_MARKERS.some((marker) => normalizedUrl.includes(marker));
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function modelMatchesKnownProviderPrefix(
|
|
947
|
-
modelNormalized: string,
|
|
948
|
-
provider: ProviderConfig,
|
|
949
|
-
): boolean {
|
|
950
|
-
if (isZhipuProvider(provider)) {
|
|
951
|
-
return ZHIPU_MODEL_PREFIXES.some((prefix) => modelNormalized.startsWith(prefix));
|
|
952
|
-
}
|
|
953
|
-
return false;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Finds a provider by model name using three strategies:
|
|
958
|
-
* 1. Case-insensitive prefix match against "{provider.name}-" (e.g., "deepseek-" matches "deepseek-*")
|
|
959
|
-
* 2. Case-insensitive match against provider.model field (with whitespace/hyphen normalization)
|
|
960
|
-
* 3. Fallback: concatenate provider name with provider's model and check if the request model matches
|
|
961
|
-
* this concatenation (handles "MiniMax" + "M3" → "MiniMax-M3" pattern when case differs)
|
|
962
|
-
*/
|
|
963
|
-
export function findProviderByModel(model: string): ProviderConfig | null {
|
|
964
|
-
const providers = getProviders();
|
|
965
|
-
const modelLower = model.toLowerCase();
|
|
966
|
-
const modelNormalized = normalizeModelName(model);
|
|
967
|
-
|
|
968
|
-
for (const provider of providers) {
|
|
969
|
-
// Strategy 1: provider name prefix match
|
|
970
|
-
const providerPrefix = (provider.name + "-").toLowerCase();
|
|
971
|
-
if (modelLower.startsWith(providerPrefix)) {
|
|
972
|
-
return provider;
|
|
973
|
-
}
|
|
974
|
-
// Strategy 1b: known model-family prefixes for providers whose public
|
|
975
|
-
// model names do not start with the provider brand, e.g. ZhipuAI -> glm-*.
|
|
976
|
-
if (modelMatchesKnownProviderPrefix(modelNormalized, provider)) {
|
|
977
|
-
return provider;
|
|
978
|
-
}
|
|
979
|
-
// Strategy 2: match against provider.models array with normalization
|
|
980
|
-
const modelList = provider.models ?? (provider.model !== undefined ? [provider.model] : []);
|
|
981
|
-
for (const m of modelList) {
|
|
982
|
-
if (m !== "" && modelNormalized === normalizeModelName(m)) {
|
|
983
|
-
return provider;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
// Strategy 3: fallback - concatenate provider name with model part and compare
|
|
987
|
-
for (const m of modelList) {
|
|
988
|
-
if (m !== "") {
|
|
989
|
-
const modelPart = modelNormalized.replace(normalizeModelName(provider.name), "");
|
|
990
|
-
const concatenated = normalizeModelName(provider.name) + modelPart;
|
|
991
|
-
if (modelNormalized === concatenated) {
|
|
992
|
-
return provider;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
return null;
|
|
998
|
-
}
|