@st-gr/sail-proxy 0.9.3 → 0.9.4
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/bundled/gateway/node_modules/.package-lock.json +17 -17
- package/bundled/gateway/node_modules/form-data/CHANGELOG.md +29 -2
- package/bundled/gateway/node_modules/form-data/README.md +4 -4
- package/bundled/gateway/node_modules/form-data/lib/form_data.js +14 -2
- package/bundled/gateway/node_modules/form-data/package.json +7 -7
- package/bundled/gateway/node_modules/qs/CHANGELOG.md +16 -0
- package/bundled/gateway/node_modules/qs/README.md +1 -1
- package/bundled/gateway/node_modules/qs/dist/qs.js +15 -15
- package/bundled/gateway/node_modules/qs/eslint.config.mjs +1 -0
- package/bundled/gateway/node_modules/qs/lib/parse.js +54 -24
- package/bundled/gateway/node_modules/qs/lib/stringify.js +11 -4
- package/bundled/gateway/node_modules/qs/package.json +3 -3
- package/bundled/gateway/node_modules/qs/test/parse.js +135 -0
- package/bundled/gateway/node_modules/qs/test/stringify.js +138 -0
- package/bundled/gateway/node_modules/qs/test/utils.js +31 -3
- package/bundled/gateway/node_modules/semver/classes/range.js +17 -4
- package/bundled/gateway/node_modules/semver/package.json +2 -2
- package/bundled/gateway/node_modules/side-channel/CHANGELOG.md +10 -0
- package/bundled/gateway/node_modules/side-channel/README.md +1 -1
- package/bundled/gateway/node_modules/side-channel/index.js +5 -2
- package/bundled/gateway/node_modules/side-channel/package.json +10 -10
- package/bundled/gateway/node_modules/side-channel/test/index.js +16 -0
- package/bundled/gateway/package-lock.json +22 -22
- package/bundled/gateway/package.json +13 -5
- package/bundled/gateway/services/gateway/src/controllers/anthropicController.js +1 -1
- package/bundled/gateway/services/gateway/src/controllers/awsBedrockController.js +5 -1
- package/bundled/gateway/services/gateway/src/index.js +14 -0
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/detectors/index.js +23 -1
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/detectors/regexDetectors.js +149 -10
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/entityToggles.d.ts +24 -0
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/entityToggles.js +54 -0
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/index.d.ts +2 -1
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/index.js +441 -92
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/replacementMap.d.ts +24 -0
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/replacementMap.js +93 -4
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/replacer.d.ts +22 -0
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/replacer.js +77 -0
- package/bundled/gateway/services/gateway/src/plugins/pseudonymization/streamBuffer.js +26 -8
- package/bundled/gateway/services/gateway/src/services/awsBedrockService.js +118 -46
- package/bundled/gateway/services/gateway/src/services/betaFlagQuarantine.d.ts +18 -0
- package/bundled/gateway/services/gateway/src/services/betaFlagQuarantine.js +111 -0
- package/bundled/gateway/services/gateway/src/services/configService.d.ts +8 -0
- package/bundled/gateway/services/gateway/src/services/configService.js +61 -53
- package/bundled/gateway/services/gateway/src/utils/bedrockStreamParser.d.ts +9 -0
- package/bundled/gateway/services/gateway/src/utils/bedrockStreamParser.js +69 -3
- package/bundled/gateway/services/gateway/src/utils/betaFeatureFilter.d.ts +31 -0
- package/bundled/gateway/services/gateway/src/utils/betaFeatureFilter.js +58 -0
- package/bundled/gateway/services/gateway/src/utils/payloadLogger.d.ts +6 -0
- package/bundled/gateway/services/gateway/src/utils/payloadLogger.js +11 -7
- package/bundled/gateway/services/gateway/src/utils/pluginCacheSelector.d.ts +15 -0
- package/bundled/gateway/services/gateway/src/utils/pluginCacheSelector.js +24 -0
- package/bundled/gateway/services/gateway/src/utils/sseWriter.js +4 -2
- package/bundled/gateway/services/gateway/src/utils/upstreamErrorBody.d.ts +10 -0
- package/bundled/gateway/services/gateway/src/utils/upstreamErrorBody.js +71 -0
- package/bundled/ollama/node_modules/.package-lock.json +14 -14
- package/bundled/ollama/node_modules/form-data/CHANGELOG.md +29 -2
- package/bundled/ollama/node_modules/form-data/README.md +4 -4
- package/bundled/ollama/node_modules/form-data/lib/form_data.js +14 -2
- package/bundled/ollama/node_modules/form-data/package.json +7 -7
- package/bundled/ollama/node_modules/qs/CHANGELOG.md +16 -0
- package/bundled/ollama/node_modules/qs/README.md +1 -1
- package/bundled/ollama/node_modules/qs/dist/qs.js +15 -15
- package/bundled/ollama/node_modules/qs/eslint.config.mjs +1 -0
- package/bundled/ollama/node_modules/qs/lib/parse.js +54 -24
- package/bundled/ollama/node_modules/qs/lib/stringify.js +11 -4
- package/bundled/ollama/node_modules/qs/package.json +3 -3
- package/bundled/ollama/node_modules/qs/test/parse.js +135 -0
- package/bundled/ollama/node_modules/qs/test/stringify.js +138 -0
- package/bundled/ollama/node_modules/qs/test/utils.js +31 -3
- package/bundled/ollama/node_modules/side-channel/CHANGELOG.md +10 -0
- package/bundled/ollama/node_modules/side-channel/README.md +1 -1
- package/bundled/ollama/node_modules/side-channel/index.js +5 -2
- package/bundled/ollama/node_modules/side-channel/package.json +10 -10
- package/bundled/ollama/node_modules/side-channel/test/index.js +16 -0
- package/bundled/ollama/package-lock.json +15 -15
- package/bundled/ollama/package.json +13 -5
- package/dist/templates/api_config.template.json +266 -18
- package/node_modules/@sap-llm-gateway/service-key-parser/package.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getQuarantinedFlags = getQuarantinedFlags;
|
|
4
|
+
exports.quarantineFlags = quarantineFlags;
|
|
5
|
+
exports.clearQuarantine = clearQuarantine;
|
|
6
|
+
exports.isInvalidBetaFlagError = isInvalidBetaFlagError;
|
|
7
|
+
exports.resolveRejectedFlags = resolveRejectedFlags;
|
|
8
|
+
exports.recordBetaFlagRejection = recordBetaFlagRejection;
|
|
9
|
+
/**
|
|
10
|
+
* In-memory quarantine for anthropic_beta flags rejected by SAP AI Core.
|
|
11
|
+
*
|
|
12
|
+
* When SAP (AWS Bedrock behind it) rejects a request with HTTP 400
|
|
13
|
+
* "invalid beta flag", the flags that were sent are quarantined for that
|
|
14
|
+
* model so subsequent requests omit them and succeed. Keyed by modelId
|
|
15
|
+
* because Bedrock's flag acceptance differs per model version.
|
|
16
|
+
*
|
|
17
|
+
* Process-local and reset on restart — a self-healing stopgap, not
|
|
18
|
+
* configuration. Entries also auto-expire after 30 minutes so a
|
|
19
|
+
* false-positive detection can't silently degrade capabilities forever.
|
|
20
|
+
* Operators should promote quarantined flags to
|
|
21
|
+
* anthropic.excluded_beta_headers (admin UI, hot-reloaded) permanently.
|
|
22
|
+
*/
|
|
23
|
+
const logger_1 = require("@libs/logger");
|
|
24
|
+
const logger = (0, logger_1.getDefaultLogger)();
|
|
25
|
+
// False positives happen: an unrelated 400 whose body merely mentions
|
|
26
|
+
// "anthropic_beta" would otherwise quarantine valid flags forever (until
|
|
27
|
+
// pod restart), silently degrading capabilities like the 1M context window.
|
|
28
|
+
// A 30-minute TTL bounds the blast radius of any single misdetection. The
|
|
29
|
+
// permanent mechanism for genuinely unsupported flags is the config
|
|
30
|
+
// denylist (anthropic.excluded_beta_headers), not this in-memory cache.
|
|
31
|
+
const QUARANTINE_TTL_MS = 30 * 60 * 1000;
|
|
32
|
+
const quarantinedByModel = new Map();
|
|
33
|
+
function getQuarantinedFlags(modelId) {
|
|
34
|
+
const flags = quarantinedByModel.get(modelId);
|
|
35
|
+
if (!flags) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const live = [];
|
|
40
|
+
for (const [flag, quarantinedAt] of flags) {
|
|
41
|
+
if (now - quarantinedAt >= QUARANTINE_TTL_MS) {
|
|
42
|
+
flags.delete(flag);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
live.push(flag);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (flags.size === 0) {
|
|
49
|
+
quarantinedByModel.delete(modelId);
|
|
50
|
+
}
|
|
51
|
+
return live;
|
|
52
|
+
}
|
|
53
|
+
function quarantineFlags(modelId, flags) {
|
|
54
|
+
if (flags.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const map = quarantinedByModel.get(modelId) ?? new Map();
|
|
59
|
+
for (const flag of flags) {
|
|
60
|
+
map.set(flag, now);
|
|
61
|
+
}
|
|
62
|
+
quarantinedByModel.set(modelId, map);
|
|
63
|
+
}
|
|
64
|
+
function clearQuarantine() {
|
|
65
|
+
quarantinedByModel.clear();
|
|
66
|
+
}
|
|
67
|
+
function serializeErrorData(errorData) {
|
|
68
|
+
if (typeof errorData === 'string') {
|
|
69
|
+
return errorData;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return JSON.stringify(errorData) ?? '';
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Whether an upstream error response is a beta-flag rejection.
|
|
80
|
+
*/
|
|
81
|
+
function isInvalidBetaFlagError(status, errorData) {
|
|
82
|
+
if (status !== 400) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return /invalid beta flag|anthropic[-_]beta/i.test(serializeErrorData(errorData));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Flags to quarantine: those explicitly named in the error body when present
|
|
89
|
+
* (first-party-style errors name them), else all flags that were sent
|
|
90
|
+
* (Bedrock's bare "invalid beta flag" names none).
|
|
91
|
+
*/
|
|
92
|
+
function resolveRejectedFlags(errorData, sentFlags) {
|
|
93
|
+
if (sentFlags.length === 0) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const serialized = serializeErrorData(errorData);
|
|
97
|
+
const named = sentFlags.filter(flag => serialized.includes(flag));
|
|
98
|
+
return named.length > 0 ? named : [...sentFlags];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Record a beta-flag rejection so subsequent requests for this model omit
|
|
102
|
+
* the offending flags. Returns the quarantined flags.
|
|
103
|
+
*/
|
|
104
|
+
function recordBetaFlagRejection(modelId, errorData, sentFlags) {
|
|
105
|
+
const rejected = resolveRejectedFlags(errorData, sentFlags);
|
|
106
|
+
quarantineFlags(modelId, rejected);
|
|
107
|
+
logger.warn('BetaFlagQuarantine', `Quarantined ${rejected.length} beta flag(s) for model ${modelId} after upstream 400: ${rejected.join(', ')}. ` +
|
|
108
|
+
`Subsequent requests omit them (in-memory, resets on restart or after 30 minutes). ` +
|
|
109
|
+
`Add them to anthropic.excluded_beta_headers via the admin UI to make this permanent.`);
|
|
110
|
+
return rejected;
|
|
111
|
+
}
|
|
@@ -111,6 +111,13 @@ export declare const getAnthropicBedrockVersion: () => string;
|
|
|
111
111
|
* @returns Array of beta header values to filter out
|
|
112
112
|
*/
|
|
113
113
|
export declare const getExcludedBetaHeaders: () => string[];
|
|
114
|
+
/**
|
|
115
|
+
* Get supported (allowlisted) beta headers for Anthropic requests to SAP AI Core.
|
|
116
|
+
* When non-empty, only these beta flags are forwarded; the excluded_beta_headers
|
|
117
|
+
* denylist is still applied on top. Empty/absent means no allowlist filtering.
|
|
118
|
+
* @returns Array of allowlisted beta header values
|
|
119
|
+
*/
|
|
120
|
+
export declare const getSupportedBetaHeaders: () => string[];
|
|
114
121
|
export declare const getOpenAIDeploymentApiVersion: () => string | undefined;
|
|
115
122
|
/**
|
|
116
123
|
* Get all provider configurations
|
|
@@ -201,6 +208,7 @@ declare const _default: {
|
|
|
201
208
|
shouldEmulateStreaming: (provider: string, modelName: string) => boolean;
|
|
202
209
|
getAnthropicBedrockVersion: () => string;
|
|
203
210
|
getExcludedBetaHeaders: () => string[];
|
|
211
|
+
getSupportedBetaHeaders: () => string[];
|
|
204
212
|
getOpenAIDeploymentApiVersion: () => string | undefined;
|
|
205
213
|
getAllProviderConfigs: () => ApiConfig;
|
|
206
214
|
getModelListChanges: () => ModelListChanges;
|
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.getAccessToken = exports.getDeploymentIdCacheStatus = exports.clearDeploymentIdCache = exports.getDeploymentId = exports.getSAPAICoreConfig = exports.getHookConfig = exports.get = exports.getTimeout = exports.getCachePricingForModel = exports.getModelListChanges = exports.getAllProviderConfigs = exports.getOpenAIDeploymentApiVersion = exports.getExcludedBetaHeaders = exports.getAnthropicBedrockVersion = exports.shouldEmulateStreaming = exports.getOriginalModel = exports.getSubstitutedModel = exports.patchConfig = exports.updateConfig = exports.getConfigAsync = exports.getConfig = exports.isConfigurationReady = exports.onConfigurationReady = exports.publishModelList = void 0;
|
|
39
|
+
exports.getAccessToken = exports.getDeploymentIdCacheStatus = exports.clearDeploymentIdCache = exports.getDeploymentId = exports.getSAPAICoreConfig = exports.getHookConfig = exports.get = exports.getTimeout = exports.getCachePricingForModel = exports.getModelListChanges = exports.getAllProviderConfigs = exports.getOpenAIDeploymentApiVersion = exports.getSupportedBetaHeaders = exports.getExcludedBetaHeaders = exports.getAnthropicBedrockVersion = exports.shouldEmulateStreaming = exports.getOriginalModel = exports.getSubstitutedModel = exports.patchConfig = exports.updateConfig = exports.getConfigAsync = exports.getConfig = exports.isConfigurationReady = exports.onConfigurationReady = exports.publishModelList = void 0;
|
|
40
40
|
/**
|
|
41
41
|
* Service to manage API provider configurations including model substitutions and streaming emulation
|
|
42
42
|
*/
|
|
@@ -48,6 +48,7 @@ const pluginLoader_1 = __importDefault(require("./pluginLoader"));
|
|
|
48
48
|
const logger_1 = require("@libs/logger");
|
|
49
49
|
const logger = (0, logger_1.getDefaultLogger)();
|
|
50
50
|
const unifiedAuthConfig_1 = require("../config/unifiedAuthConfig");
|
|
51
|
+
const pluginCacheSelector_1 = require("../utils/pluginCacheSelector");
|
|
51
52
|
// Default configuration path
|
|
52
53
|
const CONFIG_FILE_PATH = process.env.CONFIG_FILE_PATH || path.join(process.cwd(), 'api_config.json');
|
|
53
54
|
// Backup configuration
|
|
@@ -66,7 +67,11 @@ let valkeySubscriber = null;
|
|
|
66
67
|
let valkeyPublisher = null;
|
|
67
68
|
let valkeyInitialized = false;
|
|
68
69
|
// Event channels
|
|
69
|
-
|
|
70
|
+
// Env-overridable so integration tests can isolate a spawned gateway on a
|
|
71
|
+
// test-scoped channel instead of broadcasting on the production channel (a
|
|
72
|
+
// test publishing here once wiped the live gateway's config mid-session —
|
|
73
|
+
// Valkey pub/sub channels are global, they are NOT scoped by database index).
|
|
74
|
+
const CONFIG_CHANGE_CHANNEL = process.env.CONFIG_CHANGE_CHANNEL || 'sap-llm-gateway:config-changed';
|
|
70
75
|
const MODEL_LIST_CHANNEL = 'sap-llm-gateway:model-list-updated';
|
|
71
76
|
const STARTUP_READY_CHANNEL = 'sap-llm-gateway:service-ready';
|
|
72
77
|
// Startup coordination
|
|
@@ -389,6 +394,37 @@ const requestInitialConfiguration = async () => {
|
|
|
389
394
|
logger.warn('ConfigService', `Failed to request initial configuration: ${error.message}`);
|
|
390
395
|
}
|
|
391
396
|
};
|
|
397
|
+
/**
|
|
398
|
+
* Reload plugin modules on a configuration change.
|
|
399
|
+
*
|
|
400
|
+
* Evicts ONLY the plugins subtree from require.cache (plugin entry files plus
|
|
401
|
+
* their plugin-internal helper modules, e.g. plugins/pseudonymization/*), then
|
|
402
|
+
* lets pluginLoader rebuild the registry. Gateway SERVICE modules are never
|
|
403
|
+
* evicted: doing so caused split-brain state — Express handlers registered at
|
|
404
|
+
* startup kept referencing the old module instances while later require()
|
|
405
|
+
* calls created fresh ones with empty caches, and the two worlds disagreed
|
|
406
|
+
* (observed as model substitution silently failing after a config-activation
|
|
407
|
+
* event until a manual restart). Service code changes are picked up by nodemon
|
|
408
|
+
* (dev) or a deploy restart (prod), never by config pushes.
|
|
409
|
+
*/
|
|
410
|
+
const reloadPluginModules = () => {
|
|
411
|
+
try {
|
|
412
|
+
const pluginsDirPrefix = path.join(__dirname, '..', 'plugins');
|
|
413
|
+
const keys = (0, pluginCacheSelector_1.selectPluginCacheKeysToClear)(Object.keys(require.cache), pluginsDirPrefix);
|
|
414
|
+
for (const key of keys) {
|
|
415
|
+
delete require.cache[key];
|
|
416
|
+
}
|
|
417
|
+
if (keys.length > 0) {
|
|
418
|
+
logger.info('ConfigService', `Cleared require cache for ${keys.length} plugin modules`);
|
|
419
|
+
}
|
|
420
|
+
const pluginLoader = require('./pluginLoader');
|
|
421
|
+
pluginLoader.reloadAll('./src/plugins');
|
|
422
|
+
logger.info('ConfigService', 'Plugins reloaded successfully');
|
|
423
|
+
}
|
|
424
|
+
catch (pluginError) {
|
|
425
|
+
logger.error('ConfigService', `Error reloading plugins: ${pluginError.message}`);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
392
428
|
/**
|
|
393
429
|
* Trigger configuration reload mechanisms (plugins, model service cache, etc.)
|
|
394
430
|
*/
|
|
@@ -399,32 +435,8 @@ const triggerConfigurationReload = async () => {
|
|
|
399
435
|
logger.reinitialize();
|
|
400
436
|
logger.info('ConfigService', 'Logger reinitialized with updated configuration');
|
|
401
437
|
}
|
|
402
|
-
// Reload
|
|
403
|
-
|
|
404
|
-
// Clear require cache for all gateway modules so updated code on disk is loaded.
|
|
405
|
-
// This enables hot-reload of service files during config push without a full process restart.
|
|
406
|
-
// Exclude configService itself — it owns startup coordination state (configurationReceived,
|
|
407
|
-
// isWaitingForAdminEvents, startupRequestSent, cachedConfig) that must survive reloads.
|
|
408
|
-
const gatewayDistPrefix = path.join(__dirname, '..').replace(/\\/g, '/');
|
|
409
|
-
const ownModulePath = __filename.replace(/\\/g, '/');
|
|
410
|
-
let cleared = 0;
|
|
411
|
-
for (const key of Object.keys(require.cache)) {
|
|
412
|
-
const normalizedKey = key.replace(/\\/g, '/');
|
|
413
|
-
if (normalizedKey.startsWith(gatewayDistPrefix) && !normalizedKey.includes('node_modules') && normalizedKey !== ownModulePath) {
|
|
414
|
-
delete require.cache[key];
|
|
415
|
-
cleared++;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
if (cleared > 0) {
|
|
419
|
-
logger.info('ConfigService', `Cleared require cache for ${cleared} gateway modules`);
|
|
420
|
-
}
|
|
421
|
-
const pluginLoader = require('./pluginLoader');
|
|
422
|
-
pluginLoader.reloadAll('./src/plugins');
|
|
423
|
-
logger.info('ConfigService', 'Plugins reloaded successfully');
|
|
424
|
-
}
|
|
425
|
-
catch (pluginError) {
|
|
426
|
-
logger.error('ConfigService', `Error reloading plugins: ${pluginError.message}`);
|
|
427
|
-
}
|
|
438
|
+
// Reload PLUGIN modules to pick up plugin code/config changes.
|
|
439
|
+
reloadPluginModules();
|
|
428
440
|
// Clear model service caches to force reapplication of config
|
|
429
441
|
try {
|
|
430
442
|
const modelService = require('./modelService').default;
|
|
@@ -789,31 +801,9 @@ const updateConfig = (newConfig) => {
|
|
|
789
801
|
logger.reinitialize();
|
|
790
802
|
logger.info('ConfigService', 'Logger reinitialized with updated configuration');
|
|
791
803
|
}
|
|
792
|
-
// Reload
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
// This enables hot-reload of service files during config push without a full process restart.
|
|
796
|
-
// Exclude configService itself — it owns startup coordination state that must survive reloads.
|
|
797
|
-
const gatewayDistPrefix = path.join(__dirname, '..').replace(/\\/g, '/');
|
|
798
|
-
const ownModulePath = __filename.replace(/\\/g, '/');
|
|
799
|
-
let cleared = 0;
|
|
800
|
-
for (const key of Object.keys(require.cache)) {
|
|
801
|
-
const normalizedKey = key.replace(/\\/g, '/');
|
|
802
|
-
if (normalizedKey.startsWith(gatewayDistPrefix) && !normalizedKey.includes('node_modules') && normalizedKey !== ownModulePath) {
|
|
803
|
-
delete require.cache[key];
|
|
804
|
-
cleared++;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
if (cleared > 0) {
|
|
808
|
-
logger.info('ConfigService', `Cleared require cache for ${cleared} gateway modules`);
|
|
809
|
-
}
|
|
810
|
-
const pluginLoader = require('./pluginLoader');
|
|
811
|
-
pluginLoader.reloadAll('./src/plugins');
|
|
812
|
-
logger.info('ConfigService', 'Plugins reloaded successfully');
|
|
813
|
-
}
|
|
814
|
-
catch (pluginError) {
|
|
815
|
-
logger.error('ConfigService', `Error reloading plugins: ${pluginError.message}`);
|
|
816
|
-
}
|
|
804
|
+
// Reload PLUGIN modules to pick up plugin code/config changes (services are
|
|
805
|
+
// never evicted — see reloadPluginModules for the split-brain rationale).
|
|
806
|
+
reloadPluginModules();
|
|
817
807
|
// Clear model service caches to force reapplication of config
|
|
818
808
|
try {
|
|
819
809
|
const modelService = require('./modelService').default;
|
|
@@ -973,6 +963,23 @@ const getExcludedBetaHeaders = () => {
|
|
|
973
963
|
}
|
|
974
964
|
};
|
|
975
965
|
exports.getExcludedBetaHeaders = getExcludedBetaHeaders;
|
|
966
|
+
/**
|
|
967
|
+
* Get supported (allowlisted) beta headers for Anthropic requests to SAP AI Core.
|
|
968
|
+
* When non-empty, only these beta flags are forwarded; the excluded_beta_headers
|
|
969
|
+
* denylist is still applied on top. Empty/absent means no allowlist filtering.
|
|
970
|
+
* @returns Array of allowlisted beta header values
|
|
971
|
+
*/
|
|
972
|
+
const getSupportedBetaHeaders = () => {
|
|
973
|
+
try {
|
|
974
|
+
const config = (0, exports.getConfig)();
|
|
975
|
+
return config?.api_config?.anthropic?.supported_beta_headers || [];
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
logger.error('ConfigService', `Error getting supported beta headers: ${error.message}`);
|
|
979
|
+
return [];
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
exports.getSupportedBetaHeaders = getSupportedBetaHeaders;
|
|
976
983
|
const getOpenAIDeploymentApiVersion = () => {
|
|
977
984
|
try {
|
|
978
985
|
const config = (0, exports.getConfig)();
|
|
@@ -1284,6 +1291,7 @@ exports.default = {
|
|
|
1284
1291
|
shouldEmulateStreaming: exports.shouldEmulateStreaming,
|
|
1285
1292
|
getAnthropicBedrockVersion: exports.getAnthropicBedrockVersion,
|
|
1286
1293
|
getExcludedBetaHeaders: exports.getExcludedBetaHeaders,
|
|
1294
|
+
getSupportedBetaHeaders: exports.getSupportedBetaHeaders,
|
|
1287
1295
|
getOpenAIDeploymentApiVersion: exports.getOpenAIDeploymentApiVersion,
|
|
1288
1296
|
getAllProviderConfigs: exports.getAllProviderConfigs,
|
|
1289
1297
|
getModelListChanges: exports.getModelListChanges,
|
|
@@ -32,6 +32,15 @@ declare class BedrockStreamParser {
|
|
|
32
32
|
* Helper to create a content_block_start event
|
|
33
33
|
*/
|
|
34
34
|
private createContentBlockStartEvent;
|
|
35
|
+
/**
|
|
36
|
+
* Emit an event that is already in Anthropic Messages streaming format with
|
|
37
|
+
* proper `event: <type>` SSE framing. Records upstream message id and terminal
|
|
38
|
+
* usage metrics into streamState so downstream bookkeeping (usage tracking,
|
|
39
|
+
* end-of-stream finalization) stays consistent. Passthrough events never feed
|
|
40
|
+
* the tool-fragment reconstruction buffers — those exist only for Bedrock
|
|
41
|
+
* camelCase envelope streams.
|
|
42
|
+
*/
|
|
43
|
+
private emitAnthropicPassthroughEvent;
|
|
35
44
|
/**
|
|
36
45
|
* Handle end of stream
|
|
37
46
|
*/
|
|
@@ -37,6 +37,19 @@ const eventsource_parser_1 = require("eventsource-parser");
|
|
|
37
37
|
const sseWriter = __importStar(require("./sseWriter"));
|
|
38
38
|
const logger_1 = require("@libs/logger");
|
|
39
39
|
const logger = (0, logger_1.getDefaultLogger)();
|
|
40
|
+
// Anthropic Messages streaming event types. SAP native invoke-with-response-stream
|
|
41
|
+
// for deployed Anthropic models emits events ALREADY in this format — they must be
|
|
42
|
+
// passed through with proper `event: <type>` SSE framing, not transformed.
|
|
43
|
+
const ANTHROPIC_STREAM_EVENTS = new Set([
|
|
44
|
+
'message_start',
|
|
45
|
+
'content_block_start',
|
|
46
|
+
'content_block_delta',
|
|
47
|
+
'content_block_stop',
|
|
48
|
+
'message_delta',
|
|
49
|
+
'message_stop',
|
|
50
|
+
'ping',
|
|
51
|
+
'error',
|
|
52
|
+
]);
|
|
40
53
|
/**
|
|
41
54
|
* Handles streaming responses from AWS Bedrock, properly parsing SSE events and processing JSON fragments
|
|
42
55
|
*/
|
|
@@ -61,7 +74,8 @@ class BedrockStreamParser {
|
|
|
61
74
|
finalFirstByteLatency: null,
|
|
62
75
|
// Tool fragment buffering state
|
|
63
76
|
toolFragmentBuffers: new Map(), // Map of contentBlockIndex -> array of fragments
|
|
64
|
-
processedFragmentCount: 0
|
|
77
|
+
processedFragmentCount: 0,
|
|
78
|
+
terminalMessageStopEmitted: false
|
|
65
79
|
};
|
|
66
80
|
// Create our event source parser with the correct v3.0.2 API
|
|
67
81
|
this.parser = (0, eventsource_parser_1.createParser)({
|
|
@@ -103,6 +117,10 @@ class BedrockStreamParser {
|
|
|
103
117
|
return;
|
|
104
118
|
}
|
|
105
119
|
// Check if this is an SSE data chunk
|
|
120
|
+
// TODO: this heuristic misroutes network chunks that split an SSE event mid-JSON
|
|
121
|
+
// (a fragment not starting with 'data:'/'event:' is treated as direct data and can
|
|
122
|
+
// emit corrupted events). parser.feed() below already reassembles across chunk
|
|
123
|
+
// boundaries — partial SSE fragments should be fed to it rather than handleData.
|
|
106
124
|
const isSseFormat = chunkStr.startsWith('data:') ||
|
|
107
125
|
chunkStr.includes('\ndata:') ||
|
|
108
126
|
chunkStr.includes('event:') ||
|
|
@@ -277,6 +295,17 @@ class BedrockStreamParser {
|
|
|
277
295
|
return;
|
|
278
296
|
}
|
|
279
297
|
}
|
|
298
|
+
// Already-Anthropic events (SAP native streams for deployed Anthropic models)
|
|
299
|
+
// must be passed through with proper `event: <type>` SSE framing.
|
|
300
|
+
// transformBedrockEvent only understands the Bedrock camelCase envelope, so
|
|
301
|
+
// until this branch existed these all fell through to the raw `data:`-only
|
|
302
|
+
// fallback below — SSE without `event:` headers, which claude-cli >= 2.1.215
|
|
303
|
+
// cannot parse (perceived as a lost connection / ECONNRESET on every stream).
|
|
304
|
+
if (typeof bedrockEvent.type === 'string'
|
|
305
|
+
&& ANTHROPIC_STREAM_EVENTS.has(bedrockEvent.type)) {
|
|
306
|
+
this.emitAnthropicPassthroughEvent(bedrockEvent);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
280
309
|
// Transform the event
|
|
281
310
|
const events = this.transformBedrockEvent(bedrockEvent);
|
|
282
311
|
if (events && events.length > 0) {
|
|
@@ -338,14 +367,51 @@ class BedrockStreamParser {
|
|
|
338
367
|
this.streamState.emittedToolUseContentStartIndices.add(index);
|
|
339
368
|
}
|
|
340
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Emit an event that is already in Anthropic Messages streaming format with
|
|
372
|
+
* proper `event: <type>` SSE framing. Records upstream message id and terminal
|
|
373
|
+
* usage metrics into streamState so downstream bookkeeping (usage tracking,
|
|
374
|
+
* end-of-stream finalization) stays consistent. Passthrough events never feed
|
|
375
|
+
* the tool-fragment reconstruction buffers — those exist only for Bedrock
|
|
376
|
+
* camelCase envelope streams.
|
|
377
|
+
*/
|
|
378
|
+
emitAnthropicPassthroughEvent(event) {
|
|
379
|
+
if (!this.res || this.res.writableEnded)
|
|
380
|
+
return;
|
|
381
|
+
// Adopt the upstream message id so later fallback paths never mint a second
|
|
382
|
+
// message_start for a stream that already has one.
|
|
383
|
+
if (event.type === 'message_start' && event.message?.id && !this.streamState.messageId) {
|
|
384
|
+
this.streamState.messageId = event.message.id;
|
|
385
|
+
}
|
|
386
|
+
if (event.type === 'message_stop') {
|
|
387
|
+
const metrics = event['amazon-bedrock-invocationMetrics'];
|
|
388
|
+
if (metrics) {
|
|
389
|
+
this.streamState.finalInputTokens = metrics.inputTokenCount || 0;
|
|
390
|
+
this.streamState.finalOutputTokens = metrics.outputTokenCount || 0;
|
|
391
|
+
this.streamState.finalCacheCreationTokens = metrics.cacheWriteInputTokenCount || 0;
|
|
392
|
+
this.streamState.finalCacheReadTokens = metrics.cacheReadInputTokenCount || 0;
|
|
393
|
+
this.streamState.finalLatencyMs = metrics.invocationLatency || 0;
|
|
394
|
+
this.streamState.finalFirstByteLatency = metrics.firstByteLatency || 0;
|
|
395
|
+
if (this.outputFormat === 'anthropic') {
|
|
396
|
+
// Anthropic clients get a clean message_stop; the metrics live in streamState.
|
|
397
|
+
const { ['amazon-bedrock-invocationMetrics']: _metrics, ...clean } = event;
|
|
398
|
+
event = clean;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
this.streamState.terminalMessageStopEmitted = true;
|
|
402
|
+
}
|
|
403
|
+
logger.debug('BedrockStreamParser', `Passthrough Anthropic event: ${event.type}`);
|
|
404
|
+
sseWriter.writeEventStream(this.res, event.type, JSON.stringify(event));
|
|
405
|
+
}
|
|
341
406
|
/**
|
|
342
407
|
* Handle end of stream
|
|
343
408
|
*/
|
|
344
409
|
handleDone() {
|
|
345
410
|
// Process any remaining tool fragments in buffers
|
|
346
411
|
this.processAllToolFragmentBuffers();
|
|
347
|
-
// Send final message_stop if needed
|
|
348
|
-
if (this.streamState.messageId &&
|
|
412
|
+
// Send final message_stop if needed (never after a passthrough already sent one)
|
|
413
|
+
if (this.streamState.messageId && !this.streamState.terminalMessageStopEmitted
|
|
414
|
+
&& this.res && !this.res.writableEnded) {
|
|
349
415
|
if (this.outputFormat === 'anthropic') {
|
|
350
416
|
// Anthropic format: clean message_stop without Bedrock-specific metrics
|
|
351
417
|
sseWriter.writeEventStream(this.res, 'message_stop', JSON.stringify({
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for assembling and filtering anthropic_beta feature flags
|
|
3
|
+
* before forwarding requests to SAP AI Core Bedrock deployments.
|
|
4
|
+
*
|
|
5
|
+
* Filtering semantics:
|
|
6
|
+
* 1. Allowlist (supported): when non-empty, only listed flags survive.
|
|
7
|
+
* An empty/absent allowlist means "no allowlist filtering" (legacy behavior).
|
|
8
|
+
* 2. Denylist (excluded): always applied on top of the allowlist result.
|
|
9
|
+
*
|
|
10
|
+
* @see api_config.json - anthropic.supported_beta_headers / anthropic.excluded_beta_headers
|
|
11
|
+
*/
|
|
12
|
+
export interface BetaFilterOptions {
|
|
13
|
+
/** Allowlist from api_config.anthropic.supported_beta_headers; [] disables allowlist filtering */
|
|
14
|
+
supported: string[];
|
|
15
|
+
/** Denylist from api_config.anthropic.excluded_beta_headers */
|
|
16
|
+
excluded: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse the raw anthropic-beta header value into a clean array of flags.
|
|
20
|
+
* Accepts a comma-separated string, an array of values (each possibly
|
|
21
|
+
* comma-separated), or undefined.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseAnthropicBetaHeader(headerValue: string | string[] | undefined): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Merge multiple flag lists, preserving first-seen order and deduplicating.
|
|
26
|
+
*/
|
|
27
|
+
export declare function mergeBetaFeatures(...lists: string[][]): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Apply allowlist-then-denylist filtering to a list of beta flags.
|
|
30
|
+
*/
|
|
31
|
+
export declare function filterBetaFeatures(features: string[], options: BetaFilterOptions): string[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure helpers for assembling and filtering anthropic_beta feature flags
|
|
4
|
+
* before forwarding requests to SAP AI Core Bedrock deployments.
|
|
5
|
+
*
|
|
6
|
+
* Filtering semantics:
|
|
7
|
+
* 1. Allowlist (supported): when non-empty, only listed flags survive.
|
|
8
|
+
* An empty/absent allowlist means "no allowlist filtering" (legacy behavior).
|
|
9
|
+
* 2. Denylist (excluded): always applied on top of the allowlist result.
|
|
10
|
+
*
|
|
11
|
+
* @see api_config.json - anthropic.supported_beta_headers / anthropic.excluded_beta_headers
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.parseAnthropicBetaHeader = parseAnthropicBetaHeader;
|
|
15
|
+
exports.mergeBetaFeatures = mergeBetaFeatures;
|
|
16
|
+
exports.filterBetaFeatures = filterBetaFeatures;
|
|
17
|
+
/**
|
|
18
|
+
* Parse the raw anthropic-beta header value into a clean array of flags.
|
|
19
|
+
* Accepts a comma-separated string, an array of values (each possibly
|
|
20
|
+
* comma-separated), or undefined.
|
|
21
|
+
*/
|
|
22
|
+
function parseAnthropicBetaHeader(headerValue) {
|
|
23
|
+
if (!headerValue) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const rawValues = Array.isArray(headerValue) ? headerValue : [headerValue];
|
|
27
|
+
return rawValues
|
|
28
|
+
.flatMap(value => String(value).split(','))
|
|
29
|
+
.map(flag => flag.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Merge multiple flag lists, preserving first-seen order and deduplicating.
|
|
34
|
+
*/
|
|
35
|
+
function mergeBetaFeatures(...lists) {
|
|
36
|
+
const merged = [];
|
|
37
|
+
for (const list of lists) {
|
|
38
|
+
for (const flag of list) {
|
|
39
|
+
if (!merged.includes(flag)) {
|
|
40
|
+
merged.push(flag);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return merged;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Apply allowlist-then-denylist filtering to a list of beta flags.
|
|
48
|
+
*/
|
|
49
|
+
function filterBetaFeatures(features, options) {
|
|
50
|
+
let result = features;
|
|
51
|
+
if (options.supported.length > 0) {
|
|
52
|
+
result = result.filter(flag => options.supported.includes(flag));
|
|
53
|
+
}
|
|
54
|
+
if (options.excluded.length > 0) {
|
|
55
|
+
result = result.filter(flag => !options.excluded.includes(flag));
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Whether payload logging is currently enabled. Re-reads the dynamic config
|
|
4
|
+
* on every call (with the PAYLOAD_LOGGING_ENABLED env override applied), so
|
|
5
|
+
* an api_config.json change takes effect on the next request — no restart.
|
|
6
|
+
*/
|
|
7
|
+
export declare const isPayloadLoggingEnabled: () => boolean;
|
|
2
8
|
export declare const savePayload: (requestId: string | number, stage: string, data: any, req?: Request, res?: Response) => void;
|
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.savePayload = void 0;
|
|
39
|
+
exports.savePayload = exports.isPayloadLoggingEnabled = void 0;
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const logger_1 = require("@libs/logger");
|
|
@@ -131,19 +131,23 @@ function ensureLogDirExists() {
|
|
|
131
131
|
}
|
|
132
132
|
return { success: true, logDir };
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether payload logging is currently enabled. Re-reads the dynamic config
|
|
136
|
+
* on every call (with the PAYLOAD_LOGGING_ENABLED env override applied), so
|
|
137
|
+
* an api_config.json change takes effect on the next request — no restart.
|
|
138
|
+
*/
|
|
139
|
+
const isPayloadLoggingEnabled = () => {
|
|
140
|
+
return getLoggingConfig().payloadLoggingEnabled;
|
|
141
|
+
};
|
|
142
|
+
exports.isPayloadLoggingEnabled = isPayloadLoggingEnabled;
|
|
134
143
|
// Enhanced payload logger with headers and HTTP method support
|
|
135
144
|
const savePayload = (requestId, stage, data, req, res) => {
|
|
136
|
-
const currentDebugEnv = process.env.DEBUG;
|
|
137
145
|
const { payloadLoggingEnabled } = getLoggingConfig();
|
|
138
146
|
// Skip debug logging for disabled payload logging to avoid log noise at INFO level
|
|
139
147
|
if (!payloadLoggingEnabled) {
|
|
140
148
|
return;
|
|
141
149
|
}
|
|
142
|
-
logger.debug('PayloadLogger', `Enhanced save called for requestId: ${requestId}, stage: ${stage},
|
|
143
|
-
if (currentDebugEnv !== 'true') {
|
|
144
|
-
logger.debug('PayloadLogger', `Skipping enhanced save, process.env.DEBUG is not the string 'true'.`);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
150
|
+
logger.debug('PayloadLogger', `Enhanced save called for requestId: ${requestId}, stage: ${stage}, PAYLOAD_LOGGING_ENABLED: ${payloadLoggingEnabled}`);
|
|
147
151
|
if (!requestId) {
|
|
148
152
|
logger.debug('PayloadLogger', `Skipping enhanced save, no requestId provided.`);
|
|
149
153
|
return;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select which require.cache keys may be evicted on a configuration reload.
|
|
3
|
+
*
|
|
4
|
+
* ONLY modules under the plugins directory are eligible. Evicting gateway
|
|
5
|
+
* SERVICE modules on a config push causes split-brain state: Express handlers
|
|
6
|
+
* registered at startup keep referencing the old module instances, while any
|
|
7
|
+
* later require() creates fresh instances with empty caches — the two worlds
|
|
8
|
+
* then disagree (observed as model substitution silently failing after a
|
|
9
|
+
* config-activation event until a manual restart). Service code changes are
|
|
10
|
+
* picked up by nodemon (dev) or a deploy restart (prod), never by config
|
|
11
|
+
* pushes. Plugin ENTRY files are cache-cleared by pluginLoader itself; this
|
|
12
|
+
* selector additionally covers plugin-internal helper modules (e.g.
|
|
13
|
+
* plugins/pseudonymization/*.ts) that plugin entries import.
|
|
14
|
+
*/
|
|
15
|
+
export declare function selectPluginCacheKeysToClear(cacheKeys: string[], pluginsDirPrefix: string): string[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectPluginCacheKeysToClear = selectPluginCacheKeysToClear;
|
|
4
|
+
/**
|
|
5
|
+
* Select which require.cache keys may be evicted on a configuration reload.
|
|
6
|
+
*
|
|
7
|
+
* ONLY modules under the plugins directory are eligible. Evicting gateway
|
|
8
|
+
* SERVICE modules on a config push causes split-brain state: Express handlers
|
|
9
|
+
* registered at startup keep referencing the old module instances, while any
|
|
10
|
+
* later require() creates fresh instances with empty caches — the two worlds
|
|
11
|
+
* then disagree (observed as model substitution silently failing after a
|
|
12
|
+
* config-activation event until a manual restart). Service code changes are
|
|
13
|
+
* picked up by nodemon (dev) or a deploy restart (prod), never by config
|
|
14
|
+
* pushes. Plugin ENTRY files are cache-cleared by pluginLoader itself; this
|
|
15
|
+
* selector additionally covers plugin-internal helper modules (e.g.
|
|
16
|
+
* plugins/pseudonymization/*.ts) that plugin entries import.
|
|
17
|
+
*/
|
|
18
|
+
function selectPluginCacheKeysToClear(cacheKeys, pluginsDirPrefix) {
|
|
19
|
+
const prefix = pluginsDirPrefix.replace(/\\/g, '/').replace(/\/+$/, '') + '/';
|
|
20
|
+
return cacheKeys.filter(key => {
|
|
21
|
+
const normalized = key.replace(/\\/g, '/');
|
|
22
|
+
return normalized.startsWith(prefix) && !normalized.includes('node_modules');
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -176,8 +176,10 @@ const writePing = (res) => {
|
|
|
176
176
|
}
|
|
177
177
|
// Write a comment as ping (standard SSE practice)
|
|
178
178
|
res.write(': ping\n\n');
|
|
179
|
-
// Also write a
|
|
180
|
-
|
|
179
|
+
// Also write a ping event for clients that might not properly handle comments.
|
|
180
|
+
// Framed with `event: ping` like Anthropic's wire format — strict SSE parsers
|
|
181
|
+
// (claude-cli >= 2.1.215) require the event: field on every data block.
|
|
182
|
+
res.write('event: ping\ndata: {"type":"ping"}\n\n');
|
|
181
183
|
}
|
|
182
184
|
catch (writeError) {
|
|
183
185
|
logger.error('SSEWriter', 'Error writing ping to client:', writeError);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read an upstream (axios) error body that may be a stream.
|
|
3
|
+
*
|
|
4
|
+
* Streaming requests use `responseType: 'stream'`, so on an error status axios
|
|
5
|
+
* delivers `error.response.data` as a Readable stream — logging it directly
|
|
6
|
+
* prints an unusable stream object, and body-matching logic (e.g. the beta-flag
|
|
7
|
+
* quarantine) sees nothing. Drain up to a small cap with a short timeout and
|
|
8
|
+
* parse JSON when possible; non-stream bodies are returned unchanged.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readUpstreamErrorBody(response: any): Promise<any>;
|