@jsonstudio/rcc 0.89.333 → 0.89.548
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/dist/build-info.js +3 -3
- package/dist/build-info.js.map +1 -1
- package/dist/cli.js +110 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/token-daemon.d.ts +2 -0
- package/dist/commands/token-daemon.js +183 -0
- package/dist/commands/token-daemon.js.map +1 -0
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/modules/llmswitch/bridge.d.ts +1 -1
- package/dist/modules/llmswitch/bridge.js +3 -2
- package/dist/modules/llmswitch/bridge.js.map +1 -1
- package/dist/modules/pipeline/utils/colored-logger.js +3 -1
- package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
- package/dist/providers/auth/gemini-cli-userinfo-helper.js +12 -2
- package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle.js +337 -25
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/core/config/oauth-flows.d.ts +23 -0
- package/dist/providers/core/config/oauth-flows.js +92 -5
- package/dist/providers/core/config/oauth-flows.js.map +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js +9 -3
- package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
- package/dist/providers/core/config/service-profiles.js +18 -10
- package/dist/providers/core/config/service-profiles.js.map +1 -1
- package/dist/providers/core/runtime/base-provider.d.ts +2 -0
- package/dist/providers/core/runtime/base-provider.js +35 -1
- package/dist/providers/core/runtime/base-provider.js.map +1 -1
- package/dist/providers/core/runtime/gemini-cli-http-provider.js +87 -20
- package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.d.ts +1 -0
- package/dist/providers/core/runtime/http-request-executor.js +75 -1
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.d.ts +2 -0
- package/dist/providers/core/runtime/http-transport-provider.js +60 -2
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/iflow-http-provider.d.ts +4 -0
- package/dist/providers/core/runtime/iflow-http-provider.js +28 -0
- package/dist/providers/core/runtime/iflow-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/rate-limit-manager.d.ts +30 -0
- package/dist/providers/core/runtime/rate-limit-manager.js +136 -0
- package/dist/providers/core/runtime/rate-limit-manager.js.map +1 -0
- package/dist/providers/core/runtime/responses-provider.js +8 -3
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/providers/core/runtime/vision-debug-utils.d.ts +13 -0
- package/dist/providers/core/runtime/vision-debug-utils.js +114 -0
- package/dist/providers/core/runtime/vision-debug-utils.js.map +1 -0
- package/dist/providers/core/strategies/oauth-auth-code-flow.js +75 -26
- package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
- package/dist/providers/core/utils/http-client.js +2 -1
- package/dist/providers/core/utils/http-client.js.map +1 -1
- package/dist/providers/core/utils/provider-error-reporter.js +31 -5
- package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
- package/dist/providers/core/utils/provider-type-utils.js +1 -1
- package/dist/providers/core/utils/provider-type-utils.js.map +1 -1
- package/dist/providers/core/utils/snapshot-writer.d.ts +1 -1
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/server/handlers/sse-dispatcher.js +22 -2
- package/dist/server/handlers/sse-dispatcher.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +9 -0
- package/dist/server/runtime/http-server/index.js +512 -144
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/provider-utils.js +1 -1
- package/dist/server/runtime/http-server/provider-utils.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.d.ts +10 -0
- package/dist/server/runtime/http-server/request-executor.js +553 -159
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.d.ts +5 -0
- package/dist/server/runtime/http-server/routes.js +29 -0
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/runtime-manager.js +33 -0
- package/dist/server/runtime/http-server/runtime-manager.js.map +1 -1
- package/dist/server/utils/utf8-chunk-buffer.d.ts +43 -0
- package/dist/server/utils/utf8-chunk-buffer.js +132 -0
- package/dist/server/utils/utf8-chunk-buffer.js.map +1 -0
- package/dist/token-daemon/history-store.d.ts +75 -0
- package/dist/token-daemon/history-store.js +207 -0
- package/dist/token-daemon/history-store.js.map +1 -0
- package/dist/token-daemon/index.d.ts +7 -0
- package/dist/token-daemon/index.js +336 -0
- package/dist/token-daemon/index.js.map +1 -0
- package/dist/token-daemon/server-utils.d.ts +33 -0
- package/dist/token-daemon/server-utils.js +155 -0
- package/dist/token-daemon/server-utils.js.map +1 -0
- package/dist/token-daemon/token-daemon.d.ts +23 -0
- package/dist/token-daemon/token-daemon.js +249 -0
- package/dist/token-daemon/token-daemon.js.map +1 -0
- package/dist/token-daemon/token-types.d.ts +44 -0
- package/dist/token-daemon/token-types.js +18 -0
- package/dist/token-daemon/token-types.js.map +1 -0
- package/dist/token-daemon/token-utils.d.ts +17 -0
- package/dist/token-daemon/token-utils.js +153 -0
- package/dist/token-daemon/token-utils.js.map +1 -0
- package/dist/token-portal/local-token-portal.d.ts +1 -0
- package/dist/token-portal/local-token-portal.js +89 -0
- package/dist/token-portal/local-token-portal.js.map +1 -0
- package/dist/token-portal/render.d.ts +10 -0
- package/dist/token-portal/render.js +56 -0
- package/dist/token-portal/render.js.map +1 -0
- package/dist/tools/semantic-replay.js +7 -6
- package/dist/tools/semantic-replay.js.map +1 -1
- package/dist/utils/error-handler-registry.d.ts +36 -0
- package/dist/utils/error-handler-registry.js +93 -7
- package/dist/utils/error-handler-registry.js.map +1 -1
- package/node_modules/@jsonstudio/llms/README.md +2 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/codecs/gemini-openai-codec.js +137 -5
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.js +68 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.js +83 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.js +63 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-gemini.json +17 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +190 -181
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +195 -195
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/node_modules/@jsonstudio/llms/dist/conversion/config/sample-config.json +1 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +39 -4
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/target-utils.js +6 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +213 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.d.ts +34 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.js +84 -24
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.js +383 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/gemini-mapper.js +241 -14
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/standardized-bridge.js +14 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/types/standardized.d.ts +1 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +82 -3
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils.js +92 -3
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/bridge-message-utils.js +137 -10
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-output-builder.js +43 -2
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/snapshot-utils.js +17 -47
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-filter-pipeline.js +1 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-mapping.js +25 -2
- package/node_modules/@jsonstudio/llms/dist/index.d.ts +1 -0
- package/node_modules/@jsonstudio/llms/dist/index.js +1 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/bootstrap.js +308 -43
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +11 -17
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.d.ts +0 -2
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.js +0 -12
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +17 -2
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +332 -95
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/features.js +1 -1
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/message-utils.js +36 -24
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.js +2 -1
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/token-counter.js +14 -3
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +66 -2
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.js +2 -1
- package/node_modules/@jsonstudio/llms/dist/servertool/engine.d.ts +27 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +60 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.d.ts +40 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.js +1 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.d.ts +1 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.js +194 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.d.ts +1 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.js +638 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.d.ts +33 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.js +1 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/registry.d.ts +18 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/registry.js +27 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.d.ts +8 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.js +208 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +88 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/types.js +1 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.d.ts +2 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.js +185 -0
- package/node_modules/@jsonstudio/llms/dist/sse/json-to-sse/event-generators/responses.js +15 -3
- package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/builders/response-builder.js +6 -3
- package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
- package/node_modules/@jsonstudio/llms/dist/sse/types/gemini-types.d.ts +20 -1
- package/node_modules/@jsonstudio/llms/dist/sse/types/responses-types.js +1 -1
- package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.d.ts +73 -0
- package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.js +280 -0
- package/node_modules/@jsonstudio/llms/package.json +1 -1
- package/package.json +3 -2
- package/scripts/pack-mode.mjs +2 -1
- package/scripts/publish-rcc.mjs +20 -4
- package/scripts/test-iflow-web-search.mjs +141 -0
- package/scripts/test-iflow.mjs +93 -1
- package/scripts/tests/virtual-router-health.mjs +141 -6
- package/dist/tools/replay-request.d.ts +0 -0
- package/dist/tools/replay-request.js +0 -2
- package/dist/tools/replay-request.js.map +0 -1
|
@@ -47,13 +47,13 @@ export class HubRequestExecutor {
|
|
|
47
47
|
};
|
|
48
48
|
try {
|
|
49
49
|
const hubPipeline = this.ensureHubPipeline();
|
|
50
|
-
const
|
|
51
|
-
const inboundClientHeaders = this.cloneClientHeaders(
|
|
50
|
+
const initialMetadata = this.buildRequestMetadata(input);
|
|
51
|
+
const inboundClientHeaders = this.cloneClientHeaders(initialMetadata?.clientHeaders);
|
|
52
52
|
const providerRequestId = input.requestId;
|
|
53
|
-
const clientRequestId = this.resolveClientRequestId(
|
|
53
|
+
const clientRequestId = this.resolveClientRequestId(initialMetadata, providerRequestId);
|
|
54
54
|
this.logStage('request.received', providerRequestId, {
|
|
55
55
|
endpoint: input.entryEndpoint,
|
|
56
|
-
stream:
|
|
56
|
+
stream: initialMetadata.stream === true
|
|
57
57
|
});
|
|
58
58
|
try {
|
|
59
59
|
const headerUa = (typeof input.headers?.['user-agent'] === 'string' && input.headers['user-agent']) ||
|
|
@@ -66,7 +66,7 @@ export class HubRequestExecutor {
|
|
|
66
66
|
headers: asRecord(input.headers),
|
|
67
67
|
body: input.body,
|
|
68
68
|
metadata: {
|
|
69
|
-
...
|
|
69
|
+
...initialMetadata,
|
|
70
70
|
userAgent: headerUa,
|
|
71
71
|
clientOriginator: headerOriginator
|
|
72
72
|
}
|
|
@@ -76,171 +76,187 @@ export class HubRequestExecutor {
|
|
|
76
76
|
/* snapshot failure should not block request path */
|
|
77
77
|
}
|
|
78
78
|
const pipelineLabel = 'hub';
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (mergedClientHeaders) {
|
|
89
|
-
mergedMetadata.clientHeaders = mergedClientHeaders;
|
|
90
|
-
}
|
|
91
|
-
this.logStage(`${pipelineLabel}.completed`, providerRequestId, {
|
|
92
|
-
route: pipelineResult.routingDecision?.routeName,
|
|
93
|
-
target: pipelineResult.target?.providerKey
|
|
94
|
-
});
|
|
95
|
-
const providerPayload = pipelineResult.providerPayload;
|
|
96
|
-
const target = pipelineResult.target;
|
|
97
|
-
if (!providerPayload || !target?.providerKey) {
|
|
98
|
-
throw Object.assign(new Error('Virtual router did not produce a provider target'), {
|
|
99
|
-
code: 'ERR_NO_PROVIDER_TARGET',
|
|
100
|
-
requestId: input.requestId
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
const runtimeKey = target.runtimeKey || this.deps.runtimeManager.resolveRuntimeKey(target.providerKey);
|
|
104
|
-
if (!runtimeKey) {
|
|
105
|
-
throw Object.assign(new Error(`Runtime for provider ${target.providerKey} not initialized`), {
|
|
106
|
-
code: 'ERR_RUNTIME_NOT_FOUND',
|
|
107
|
-
requestId: input.requestId
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
const handle = this.deps.runtimeManager.getHandleByRuntimeKey(runtimeKey);
|
|
111
|
-
if (!handle) {
|
|
112
|
-
throw Object.assign(new Error(`Provider runtime ${runtimeKey} not found`), {
|
|
113
|
-
code: 'ERR_PROVIDER_NOT_FOUND',
|
|
114
|
-
requestId: input.requestId
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
const providerProtocol = target.outboundProfile || handle.providerProtocol;
|
|
118
|
-
const metadataModel = mergedMetadata?.target && typeof mergedMetadata.target === 'object'
|
|
119
|
-
? mergedMetadata.target.clientModelId
|
|
120
|
-
: undefined;
|
|
121
|
-
const rawModel = this.extractProviderModel(providerPayload) ||
|
|
122
|
-
(typeof metadataModel === 'string' ? metadataModel : undefined);
|
|
123
|
-
const providerAlias = typeof target.providerKey === 'string' && target.providerKey.includes('.')
|
|
124
|
-
? target.providerKey.split('.').slice(0, 2).join('.')
|
|
125
|
-
: target.providerKey;
|
|
126
|
-
const providerIdToken = providerAlias || handle.providerId || runtimeKey;
|
|
127
|
-
if (!providerIdToken) {
|
|
128
|
-
throw Object.assign(new Error('Provider identifier missing for request'), {
|
|
129
|
-
code: 'ERR_PROVIDER_ID_MISSING',
|
|
130
|
-
requestId: providerRequestId
|
|
79
|
+
let iterationMetadata = initialMetadata;
|
|
80
|
+
let aggregatedUsage;
|
|
81
|
+
let attempt = 0;
|
|
82
|
+
let followupTriggered = false;
|
|
83
|
+
while (true) {
|
|
84
|
+
this.logStage(`${pipelineLabel}.start`, providerRequestId, {
|
|
85
|
+
endpoint: input.entryEndpoint,
|
|
86
|
+
stream: iterationMetadata.stream,
|
|
87
|
+
attempt
|
|
131
88
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
this.deps.stats.bindProvider(input.requestId, {
|
|
148
|
-
providerKey: target.providerKey,
|
|
149
|
-
providerType: handle.providerType,
|
|
150
|
-
model: providerModel
|
|
151
|
-
});
|
|
152
|
-
this.logStage('provider.prepare', input.requestId, {
|
|
153
|
-
providerKey: target.providerKey,
|
|
154
|
-
runtimeKey,
|
|
155
|
-
protocol: providerProtocol,
|
|
156
|
-
providerType: handle.providerType,
|
|
157
|
-
providerFamily: handle.providerFamily,
|
|
158
|
-
model: providerModel,
|
|
159
|
-
providerLabel
|
|
160
|
-
});
|
|
161
|
-
attachProviderRuntimeMetadata(providerPayload, {
|
|
162
|
-
requestId: input.requestId,
|
|
163
|
-
providerId: handle.providerId,
|
|
164
|
-
providerKey: target.providerKey,
|
|
165
|
-
providerType: handle.providerType,
|
|
166
|
-
providerFamily: handle.providerFamily,
|
|
167
|
-
providerProtocol,
|
|
168
|
-
pipelineId: target.providerKey,
|
|
169
|
-
routeName: pipelineResult.routingDecision?.routeName,
|
|
170
|
-
runtimeKey,
|
|
171
|
-
target,
|
|
172
|
-
metadata: mergedMetadata,
|
|
173
|
-
compatibilityProfile: target.compatibilityProfile
|
|
174
|
-
});
|
|
175
|
-
this.logStage('provider.send.start', input.requestId, {
|
|
176
|
-
providerKey: target.providerKey,
|
|
177
|
-
runtimeKey,
|
|
178
|
-
protocol: providerProtocol,
|
|
179
|
-
providerType: handle.providerType,
|
|
180
|
-
providerFamily: handle.providerFamily,
|
|
181
|
-
model: providerModel,
|
|
182
|
-
providerLabel
|
|
183
|
-
});
|
|
184
|
-
try {
|
|
185
|
-
const providerResponse = await handle.instance.processIncoming(providerPayload);
|
|
186
|
-
const responseStatus = this.extractResponseStatus(providerResponse);
|
|
187
|
-
this.logStage('provider.send.completed', input.requestId, {
|
|
188
|
-
providerKey: target.providerKey,
|
|
189
|
-
status: responseStatus,
|
|
190
|
-
providerType: handle.providerType,
|
|
191
|
-
providerFamily: handle.providerFamily,
|
|
192
|
-
model: providerModel,
|
|
193
|
-
providerLabel
|
|
89
|
+
// 获取原始请求快照,用于后续响应转换(不包含任何 server-side 工具语义)。
|
|
90
|
+
const originalRequestSnapshot = this.cloneRequestPayload(input.body);
|
|
91
|
+
const pipelineResult = await this.runHubPipeline(hubPipeline, input, iterationMetadata);
|
|
92
|
+
const pipelineMetadata = pipelineResult.metadata ?? {};
|
|
93
|
+
const mergedMetadata = { ...iterationMetadata, ...pipelineMetadata };
|
|
94
|
+
const mergedClientHeaders = this.cloneClientHeaders(mergedMetadata?.clientHeaders) || inboundClientHeaders;
|
|
95
|
+
if (mergedClientHeaders) {
|
|
96
|
+
mergedMetadata.clientHeaders = mergedClientHeaders;
|
|
97
|
+
}
|
|
98
|
+
mergedMetadata.clientRequestId = clientRequestId;
|
|
99
|
+
this.logStage(`${pipelineLabel}.completed`, providerRequestId, {
|
|
100
|
+
route: pipelineResult.routingDecision?.routeName,
|
|
101
|
+
target: pipelineResult.target?.providerKey,
|
|
102
|
+
attempt
|
|
194
103
|
});
|
|
195
|
-
const
|
|
196
|
-
const
|
|
104
|
+
const providerPayload = pipelineResult.providerPayload;
|
|
105
|
+
const target = pipelineResult.target;
|
|
106
|
+
if (!providerPayload || !target?.providerKey) {
|
|
107
|
+
throw Object.assign(new Error('Virtual router did not produce a provider target'), {
|
|
108
|
+
code: 'ERR_NO_PROVIDER_TARGET',
|
|
109
|
+
requestId: input.requestId
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const runtimeKey = target.runtimeKey || this.deps.runtimeManager.resolveRuntimeKey(target.providerKey);
|
|
113
|
+
if (!runtimeKey) {
|
|
114
|
+
throw Object.assign(new Error(`Runtime for provider ${target.providerKey} not initialized`), {
|
|
115
|
+
code: 'ERR_RUNTIME_NOT_FOUND',
|
|
116
|
+
requestId: input.requestId
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const handle = this.deps.runtimeManager.getHandleByRuntimeKey(runtimeKey);
|
|
120
|
+
if (!handle) {
|
|
121
|
+
throw Object.assign(new Error(`Provider runtime ${runtimeKey} not found`), {
|
|
122
|
+
code: 'ERR_PROVIDER_NOT_FOUND',
|
|
123
|
+
requestId: input.requestId
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const providerProtocol = target.outboundProfile || handle.providerProtocol;
|
|
127
|
+
const metadataModel = mergedMetadata?.target && typeof mergedMetadata.target === 'object'
|
|
128
|
+
? mergedMetadata.target.clientModelId
|
|
129
|
+
: undefined;
|
|
130
|
+
const rawModel = this.extractProviderModel(providerPayload) ||
|
|
131
|
+
(typeof metadataModel === 'string' ? metadataModel : undefined);
|
|
132
|
+
const providerAlias = typeof target.providerKey === 'string' && target.providerKey.includes('.')
|
|
133
|
+
? target.providerKey.split('.').slice(0, 2).join('.')
|
|
134
|
+
: target.providerKey;
|
|
135
|
+
const providerIdToken = providerAlias || handle.providerId || runtimeKey;
|
|
136
|
+
if (!providerIdToken) {
|
|
137
|
+
throw Object.assign(new Error('Provider identifier missing for request'), {
|
|
138
|
+
code: 'ERR_PROVIDER_ID_MISSING',
|
|
139
|
+
requestId: providerRequestId
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const enhancedRequestId = enhanceProviderRequestId(providerRequestId, {
|
|
197
143
|
entryEndpoint: input.entryEndpoint,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
wantsStream: Boolean(input.metadata?.inboundStream ?? input.metadata?.stream),
|
|
201
|
-
originalRequest: originalRequestSnapshot,
|
|
202
|
-
processMode: pipelineResult.processMode,
|
|
203
|
-
response: normalized,
|
|
204
|
-
pipelineMetadata: mergedMetadata
|
|
144
|
+
providerId: providerIdToken,
|
|
145
|
+
model: rawModel
|
|
205
146
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
147
|
+
if (enhancedRequestId !== input.requestId) {
|
|
148
|
+
input.requestId = enhancedRequestId;
|
|
149
|
+
}
|
|
150
|
+
mergedMetadata.clientRequestId = clientRequestId;
|
|
151
|
+
const providerModel = rawModel;
|
|
152
|
+
const providerLabel = this.buildProviderLabel(target.providerKey, providerModel);
|
|
153
|
+
if (inboundClientHeaders) {
|
|
154
|
+
this.ensureClientHeadersOnPayload(providerPayload, inboundClientHeaders);
|
|
155
|
+
}
|
|
156
|
+
this.deps.stats.bindProvider(input.requestId, {
|
|
209
157
|
providerKey: target.providerKey,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
latencyMs: Date.now() - requestStartedAt
|
|
158
|
+
providerType: handle.providerType,
|
|
159
|
+
model: providerModel
|
|
213
160
|
});
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
this.logStage('provider.send.error', input.requestId, {
|
|
161
|
+
this.logStage('provider.prepare', input.requestId, {
|
|
218
162
|
providerKey: target.providerKey,
|
|
219
|
-
|
|
163
|
+
runtimeKey,
|
|
164
|
+
protocol: providerProtocol,
|
|
220
165
|
providerType: handle.providerType,
|
|
221
166
|
providerFamily: handle.providerFamily,
|
|
222
167
|
model: providerModel,
|
|
223
|
-
providerLabel
|
|
168
|
+
providerLabel,
|
|
169
|
+
attempt
|
|
224
170
|
});
|
|
225
|
-
|
|
171
|
+
attachProviderRuntimeMetadata(providerPayload, {
|
|
226
172
|
requestId: input.requestId,
|
|
227
|
-
providerKey: target.providerKey,
|
|
228
173
|
providerId: handle.providerId,
|
|
174
|
+
providerKey: target.providerKey,
|
|
229
175
|
providerType: handle.providerType,
|
|
176
|
+
providerFamily: handle.providerFamily,
|
|
230
177
|
providerProtocol,
|
|
231
|
-
routeName: pipelineResult.routingDecision?.routeName,
|
|
232
178
|
pipelineId: target.providerKey,
|
|
179
|
+
routeName: pipelineResult.routingDecision?.routeName,
|
|
233
180
|
runtimeKey,
|
|
234
|
-
target
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
emitProviderError({
|
|
238
|
-
error,
|
|
239
|
-
stage: 'provider.send',
|
|
240
|
-
runtime: runtimeMetadata,
|
|
241
|
-
dependencies: this.deps.getModuleDependencies()
|
|
181
|
+
target,
|
|
182
|
+
metadata: mergedMetadata,
|
|
183
|
+
compatibilityProfile: target.compatibilityProfile
|
|
242
184
|
});
|
|
243
|
-
|
|
185
|
+
this.logStage('provider.send.start', input.requestId, {
|
|
186
|
+
providerKey: target.providerKey,
|
|
187
|
+
runtimeKey,
|
|
188
|
+
protocol: providerProtocol,
|
|
189
|
+
providerType: handle.providerType,
|
|
190
|
+
providerFamily: handle.providerFamily,
|
|
191
|
+
model: providerModel,
|
|
192
|
+
providerLabel,
|
|
193
|
+
attempt
|
|
194
|
+
});
|
|
195
|
+
try {
|
|
196
|
+
const providerResponse = await handle.instance.processIncoming(providerPayload);
|
|
197
|
+
const responseStatus = this.extractResponseStatus(providerResponse);
|
|
198
|
+
this.logStage('provider.send.completed', input.requestId, {
|
|
199
|
+
providerKey: target.providerKey,
|
|
200
|
+
status: responseStatus,
|
|
201
|
+
providerType: handle.providerType,
|
|
202
|
+
providerFamily: handle.providerFamily,
|
|
203
|
+
model: providerModel,
|
|
204
|
+
providerLabel,
|
|
205
|
+
attempt
|
|
206
|
+
});
|
|
207
|
+
const wantsStreamBase = Boolean(input.metadata?.inboundStream ?? input.metadata?.stream);
|
|
208
|
+
const normalized = this.normalizeProviderResponse(providerResponse);
|
|
209
|
+
const converted = await this.convertProviderResponseIfNeeded({
|
|
210
|
+
entryEndpoint: input.entryEndpoint,
|
|
211
|
+
providerType: handle.providerType,
|
|
212
|
+
requestId: input.requestId,
|
|
213
|
+
wantsStream: wantsStreamBase,
|
|
214
|
+
originalRequest: originalRequestSnapshot,
|
|
215
|
+
processMode: pipelineResult.processMode,
|
|
216
|
+
response: normalized,
|
|
217
|
+
pipelineMetadata: mergedMetadata
|
|
218
|
+
});
|
|
219
|
+
const usage = this.extractUsageFromResult(converted, mergedMetadata);
|
|
220
|
+
aggregatedUsage = this.mergeUsageMetrics(aggregatedUsage, usage);
|
|
221
|
+
finalizeStats({ usage: aggregatedUsage, error: false });
|
|
222
|
+
this.logUsageSummary(input.requestId, {
|
|
223
|
+
providerKey: target.providerKey,
|
|
224
|
+
model: providerModel,
|
|
225
|
+
usage: aggregatedUsage,
|
|
226
|
+
latencyMs: Date.now() - requestStartedAt
|
|
227
|
+
});
|
|
228
|
+
return converted;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
this.logStage('provider.send.error', input.requestId, {
|
|
232
|
+
providerKey: target.providerKey,
|
|
233
|
+
message: error instanceof Error ? error.message : String(error ?? 'Unknown error'),
|
|
234
|
+
providerType: handle.providerType,
|
|
235
|
+
providerFamily: handle.providerFamily,
|
|
236
|
+
model: providerModel,
|
|
237
|
+
providerLabel,
|
|
238
|
+
attempt
|
|
239
|
+
});
|
|
240
|
+
const runtimeMetadata = {
|
|
241
|
+
requestId: input.requestId,
|
|
242
|
+
providerKey: target.providerKey,
|
|
243
|
+
providerId: handle.providerId,
|
|
244
|
+
providerType: handle.providerType,
|
|
245
|
+
providerProtocol,
|
|
246
|
+
routeName: pipelineResult.routingDecision?.routeName,
|
|
247
|
+
pipelineId: target.providerKey,
|
|
248
|
+
runtimeKey,
|
|
249
|
+
target
|
|
250
|
+
};
|
|
251
|
+
runtimeMetadata.providerFamily = handle.providerFamily;
|
|
252
|
+
emitProviderError({
|
|
253
|
+
error,
|
|
254
|
+
stage: 'provider.send',
|
|
255
|
+
runtime: runtimeMetadata,
|
|
256
|
+
dependencies: this.deps.getModuleDependencies()
|
|
257
|
+
});
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
244
260
|
}
|
|
245
261
|
}
|
|
246
262
|
catch (error) {
|
|
@@ -412,12 +428,17 @@ export class HubRequestExecutor {
|
|
|
412
428
|
const metadataBag = asRecord(options.pipelineMetadata);
|
|
413
429
|
const aliasMap = extractAnthropicToolAliasMap(metadataBag);
|
|
414
430
|
const originalModelId = this.extractClientModelId(metadataBag, options.originalRequest);
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
entryEndpoint: options.entryEndpoint || entry,
|
|
418
|
-
providerProtocol,
|
|
419
|
-
originalModelId
|
|
431
|
+
const baseContext = {
|
|
432
|
+
...(metadataBag ?? {})
|
|
420
433
|
};
|
|
434
|
+
if (typeof metadataBag?.routeName === 'string') {
|
|
435
|
+
baseContext.routeId = metadataBag.routeName;
|
|
436
|
+
}
|
|
437
|
+
baseContext.requestId = options.requestId;
|
|
438
|
+
baseContext.entryEndpoint = options.entryEndpoint || entry;
|
|
439
|
+
baseContext.providerProtocol = providerProtocol;
|
|
440
|
+
baseContext.originalModelId = originalModelId;
|
|
441
|
+
const adapterContext = baseContext;
|
|
421
442
|
if (aliasMap) {
|
|
422
443
|
adapterContext.anthropicToolNameMap = aliasMap;
|
|
423
444
|
}
|
|
@@ -425,14 +446,85 @@ export class HubRequestExecutor {
|
|
|
425
446
|
loadConvertProviderResponse(),
|
|
426
447
|
loadSnapshotRecorderFactory()
|
|
427
448
|
]);
|
|
428
|
-
const stageRecorder = createSnapshotRecorder(adapterContext, adapterContext.entryEndpoint
|
|
449
|
+
const stageRecorder = createSnapshotRecorder(adapterContext, typeof adapterContext.entryEndpoint === 'string'
|
|
450
|
+
? adapterContext.entryEndpoint
|
|
451
|
+
: options.entryEndpoint || entry);
|
|
452
|
+
const providerInvoker = async (invokeOptions) => {
|
|
453
|
+
// 将 server-side 工具的 routeHint 注入到内部 payload 的 metadata,
|
|
454
|
+
// 以便后续在标准 HubPipeline 中保持路由上下文一致(例如强制 web_search)。
|
|
455
|
+
if (invokeOptions.routeHint) {
|
|
456
|
+
const carrier = invokeOptions.payload;
|
|
457
|
+
const existingMeta = carrier.metadata && typeof carrier.metadata === 'object'
|
|
458
|
+
? carrier.metadata
|
|
459
|
+
: {};
|
|
460
|
+
carrier.metadata = {
|
|
461
|
+
...existingMeta,
|
|
462
|
+
routeHint: existingMeta.routeHint ?? invokeOptions.routeHint
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
// Delegate to existing runtimeManager / Provider V2 stack.
|
|
466
|
+
const runtimeKey = this.deps.runtimeManager.resolveRuntimeKey(invokeOptions.providerKey);
|
|
467
|
+
if (!runtimeKey) {
|
|
468
|
+
throw new Error(`Runtime for provider ${invokeOptions.providerKey} not initialized`);
|
|
469
|
+
}
|
|
470
|
+
const handle = this.deps.runtimeManager.getHandleByRuntimeKey(runtimeKey);
|
|
471
|
+
if (!handle) {
|
|
472
|
+
throw new Error(`Provider runtime ${runtimeKey} not found`);
|
|
473
|
+
}
|
|
474
|
+
const providerResponse = await handle.instance.processIncoming(invokeOptions.payload);
|
|
475
|
+
const normalized = this.normalizeProviderResponse(providerResponse);
|
|
476
|
+
const bodyPayload = normalized.body && typeof normalized.body === 'object'
|
|
477
|
+
? normalized.body
|
|
478
|
+
: normalized;
|
|
479
|
+
return { providerResponse: bodyPayload };
|
|
480
|
+
};
|
|
481
|
+
const reenterPipeline = async (reenterOpts) => {
|
|
482
|
+
const nestedEntry = reenterOpts.entryEndpoint || options.entryEndpoint || entry;
|
|
483
|
+
const nestedExtra = asRecord(reenterOpts.metadata) ?? {};
|
|
484
|
+
const nestedEntryLower = nestedEntry.toLowerCase();
|
|
485
|
+
// 基于首次 HubPipeline metadata + 调用方注入的 metadata 构建新的请求 metadata。
|
|
486
|
+
// 不在 Host 层编码 servertool/web_search 等语义,由 llmswitch-core 负责。
|
|
487
|
+
const nestedMetadata = {
|
|
488
|
+
...(metadataBag ?? {}),
|
|
489
|
+
...nestedExtra,
|
|
490
|
+
entryEndpoint: nestedEntry,
|
|
491
|
+
direction: 'request',
|
|
492
|
+
stage: 'inbound'
|
|
493
|
+
};
|
|
494
|
+
// 针对 reenterPipeline 的入口端点,纠正 providerProtocol,避免沿用外层协议。
|
|
495
|
+
if (nestedEntryLower.includes('/v1/chat/completions')) {
|
|
496
|
+
nestedMetadata.providerProtocol = 'openai-chat';
|
|
497
|
+
}
|
|
498
|
+
else if (nestedEntryLower.includes('/v1/responses')) {
|
|
499
|
+
nestedMetadata.providerProtocol = 'openai-responses';
|
|
500
|
+
}
|
|
501
|
+
else if (nestedEntryLower.includes('/v1/messages')) {
|
|
502
|
+
nestedMetadata.providerProtocol = 'anthropic-messages';
|
|
503
|
+
}
|
|
504
|
+
const nestedInput = {
|
|
505
|
+
entryEndpoint: nestedEntry,
|
|
506
|
+
method: 'POST',
|
|
507
|
+
requestId: reenterOpts.requestId,
|
|
508
|
+
headers: {},
|
|
509
|
+
query: {},
|
|
510
|
+
body: reenterOpts.body,
|
|
511
|
+
metadata: nestedMetadata
|
|
512
|
+
};
|
|
513
|
+
const nestedResult = await this.execute(nestedInput);
|
|
514
|
+
const nestedBody = nestedResult.body && typeof nestedResult.body === 'object'
|
|
515
|
+
? nestedResult.body
|
|
516
|
+
: undefined;
|
|
517
|
+
return { body: nestedBody };
|
|
518
|
+
};
|
|
429
519
|
const converted = await convertProviderResponse({
|
|
430
520
|
providerProtocol,
|
|
431
521
|
providerResponse: body,
|
|
432
522
|
context: adapterContext,
|
|
433
523
|
entryEndpoint: options.entryEndpoint || entry,
|
|
434
524
|
wantsStream: options.wantsStream,
|
|
435
|
-
|
|
525
|
+
providerInvoker,
|
|
526
|
+
stageRecorder,
|
|
527
|
+
reenterPipeline
|
|
436
528
|
});
|
|
437
529
|
if (converted.__sse_responses) {
|
|
438
530
|
return {
|
|
@@ -446,6 +538,20 @@ export class HubRequestExecutor {
|
|
|
446
538
|
};
|
|
447
539
|
}
|
|
448
540
|
catch (error) {
|
|
541
|
+
const err = error;
|
|
542
|
+
const message = err instanceof Error ? err.message : String(err ?? 'Unknown error');
|
|
543
|
+
const providerProtocol = mapProviderProtocol(options.providerType);
|
|
544
|
+
// 对于 Gemini 等基于 SSE 的 provider,如果 llmswitch-core 报告
|
|
545
|
+
// “Failed to convert SSE payload ...” 之类错误,说明上游流式响应已异常
|
|
546
|
+
// 终止(如 Cloud Code 终止/上下文超限),继续回退到原始 SSE 只会让
|
|
547
|
+
// 客户端挂起。因此在此类场景下直接抛出错误,让 HTTP 层返回明确的
|
|
548
|
+
// 5xx/4xx,而不是静默退回原始 payload。
|
|
549
|
+
const isSseConvertFailure = typeof message === 'string' &&
|
|
550
|
+
message.toLowerCase().includes('failed to convert sse payload');
|
|
551
|
+
if (providerProtocol === 'gemini-chat' && isSseConvertFailure) {
|
|
552
|
+
console.error('[RequestExecutor] Fatal SSE decode error for Gemini provider, bubbling as HTTP error', error);
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
449
555
|
console.error('[RequestExecutor] Failed to convert provider response via llmswitch-core', error);
|
|
450
556
|
return options.response;
|
|
451
557
|
}
|
|
@@ -579,6 +685,294 @@ export class HubRequestExecutor {
|
|
|
579
685
|
total_tokens: total
|
|
580
686
|
};
|
|
581
687
|
}
|
|
688
|
+
mergeUsageMetrics(base, delta) {
|
|
689
|
+
if (!delta) {
|
|
690
|
+
return base;
|
|
691
|
+
}
|
|
692
|
+
if (!base) {
|
|
693
|
+
return { ...delta };
|
|
694
|
+
}
|
|
695
|
+
const merged = {
|
|
696
|
+
prompt_tokens: (base.prompt_tokens ?? 0) + (delta.prompt_tokens ?? 0),
|
|
697
|
+
completion_tokens: (base.completion_tokens ?? 0) + (delta.completion_tokens ?? 0)
|
|
698
|
+
};
|
|
699
|
+
const total = (base.total_tokens ?? 0) + (delta.total_tokens ?? 0);
|
|
700
|
+
merged.total_tokens = total || undefined;
|
|
701
|
+
return merged;
|
|
702
|
+
}
|
|
703
|
+
buildVisionFollowupPayload(options) {
|
|
704
|
+
const { originalPayload, visionResponse } = options;
|
|
705
|
+
if (!originalPayload || typeof originalPayload !== 'object') {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const clone = this.cloneRequestPayload(originalPayload) ?? { ...originalPayload };
|
|
709
|
+
if (!clone) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
const visionText = this.extractVisionDescription(visionResponse?.body);
|
|
713
|
+
if (!visionText) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
if (this.rewriteResponsesInput(clone, visionText)) {
|
|
717
|
+
return clone;
|
|
718
|
+
}
|
|
719
|
+
if (this.rewriteChatMessages(clone, visionText)) {
|
|
720
|
+
return clone;
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
rewriteResponsesInput(payload, visionText) {
|
|
725
|
+
const inputList = payload.input;
|
|
726
|
+
if (!Array.isArray(inputList)) {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
for (let i = inputList.length - 1; i >= 0; i -= 1) {
|
|
730
|
+
const item = inputList[i];
|
|
731
|
+
if (!item || typeof item !== 'object') {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const role = typeof item.role === 'string'
|
|
735
|
+
? item.role
|
|
736
|
+
: '';
|
|
737
|
+
if (role !== 'user') {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const contentBlocks = Array.isArray(item.content)
|
|
741
|
+
? [...item.content]
|
|
742
|
+
: [];
|
|
743
|
+
const originalText = this.extractTextFromContentBlocks(contentBlocks, ['input_text', 'text']);
|
|
744
|
+
const textType = this.detectContentTextType(contentBlocks, 'input_text');
|
|
745
|
+
const composed = this.composeVisionUserText(visionText, originalText);
|
|
746
|
+
item.content = [
|
|
747
|
+
{
|
|
748
|
+
type: textType,
|
|
749
|
+
text: composed
|
|
750
|
+
}
|
|
751
|
+
];
|
|
752
|
+
inputList[i] = item;
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
rewriteChatMessages(payload, visionText) {
|
|
758
|
+
const messages = payload.messages;
|
|
759
|
+
if (!Array.isArray(messages)) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
763
|
+
const message = messages[i];
|
|
764
|
+
if (!message || typeof message !== 'object') {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
const role = typeof message.role === 'string'
|
|
768
|
+
? message.role
|
|
769
|
+
: '';
|
|
770
|
+
if (role !== 'user') {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const contentBlocks = Array.isArray(message.content)
|
|
774
|
+
? [...message.content]
|
|
775
|
+
: [];
|
|
776
|
+
const originalText = this.extractTextFromContentBlocks(contentBlocks, ['text']);
|
|
777
|
+
const textType = this.detectContentTextType(contentBlocks, 'text');
|
|
778
|
+
const composed = this.composeVisionUserText(visionText, originalText);
|
|
779
|
+
message.content = [
|
|
780
|
+
{
|
|
781
|
+
type: textType,
|
|
782
|
+
text: composed
|
|
783
|
+
}
|
|
784
|
+
];
|
|
785
|
+
messages[i] = message;
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
extractTextFromContentBlocks(content, allowedTypes) {
|
|
791
|
+
if (typeof content === 'string') {
|
|
792
|
+
return content;
|
|
793
|
+
}
|
|
794
|
+
if (!Array.isArray(content)) {
|
|
795
|
+
return '';
|
|
796
|
+
}
|
|
797
|
+
const collected = [];
|
|
798
|
+
for (const block of content) {
|
|
799
|
+
if (!block || typeof block !== 'object') {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const typeValue = typeof block.type === 'string'
|
|
803
|
+
? block.type
|
|
804
|
+
: '';
|
|
805
|
+
if (allowedTypes.length && !allowedTypes.includes(typeValue)) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const textValue = block.text;
|
|
809
|
+
if (typeof textValue === 'string' && textValue.trim()) {
|
|
810
|
+
collected.push(textValue.trim());
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return collected.join('\n');
|
|
814
|
+
}
|
|
815
|
+
detectContentTextType(content, fallback) {
|
|
816
|
+
if (Array.isArray(content)) {
|
|
817
|
+
for (const block of content) {
|
|
818
|
+
if (!block || typeof block !== 'object') {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const typeValue = block.type;
|
|
822
|
+
if (typeof typeValue === 'string' && (typeValue === 'text' || typeValue === 'input_text')) {
|
|
823
|
+
return typeValue;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return fallback;
|
|
828
|
+
}
|
|
829
|
+
composeVisionUserText(visionText, originalText) {
|
|
830
|
+
const sections = [];
|
|
831
|
+
const cleanedVision = (visionText || '').trim();
|
|
832
|
+
if (cleanedVision) {
|
|
833
|
+
sections.push(`【图片分析】\n${cleanedVision}`);
|
|
834
|
+
}
|
|
835
|
+
const cleanedOriginal = (originalText || '').trim();
|
|
836
|
+
if (cleanedOriginal) {
|
|
837
|
+
sections.push(`【用户原始请求】\n${cleanedOriginal}`);
|
|
838
|
+
}
|
|
839
|
+
return sections.join('\n\n');
|
|
840
|
+
}
|
|
841
|
+
extractVisionDescription(body) {
|
|
842
|
+
if (!body) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
if (typeof body === 'string') {
|
|
846
|
+
const trimmed = body.trim();
|
|
847
|
+
return trimmed.length ? trimmed : null;
|
|
848
|
+
}
|
|
849
|
+
if (typeof body !== 'object') {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
const record = body;
|
|
853
|
+
const direct = this.extractTextCandidate(record);
|
|
854
|
+
if (direct) {
|
|
855
|
+
return direct;
|
|
856
|
+
}
|
|
857
|
+
if (record.response && typeof record.response === 'object') {
|
|
858
|
+
const responseNode = record.response;
|
|
859
|
+
const nested = this.extractTextCandidate(responseNode);
|
|
860
|
+
if (nested) {
|
|
861
|
+
return nested;
|
|
862
|
+
}
|
|
863
|
+
const output = responseNode.output;
|
|
864
|
+
if (Array.isArray(output)) {
|
|
865
|
+
for (const entry of output) {
|
|
866
|
+
if (entry && typeof entry === 'object') {
|
|
867
|
+
const nestedText = this.extractTextCandidate(entry);
|
|
868
|
+
if (nestedText) {
|
|
869
|
+
return nestedText;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (Array.isArray(record.output)) {
|
|
876
|
+
for (const entry of record.output) {
|
|
877
|
+
if (entry && typeof entry === 'object') {
|
|
878
|
+
const nested = this.extractTextCandidate(entry);
|
|
879
|
+
if (nested) {
|
|
880
|
+
return nested;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const choices = record.choices;
|
|
886
|
+
if (Array.isArray(choices)) {
|
|
887
|
+
for (const choice of choices) {
|
|
888
|
+
if (!choice || typeof choice !== 'object') {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const message = choice.message;
|
|
892
|
+
if (message && typeof message === 'object') {
|
|
893
|
+
const msg = message;
|
|
894
|
+
const content = msg.content;
|
|
895
|
+
if (typeof content === 'string' && content.trim()) {
|
|
896
|
+
return content.trim();
|
|
897
|
+
}
|
|
898
|
+
if (Array.isArray(content)) {
|
|
899
|
+
for (const part of content) {
|
|
900
|
+
if (part && typeof part === 'object' && typeof part.text === 'string') {
|
|
901
|
+
const textValue = part.text.trim();
|
|
902
|
+
if (textValue) {
|
|
903
|
+
return textValue;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
extractTextCandidate(record) {
|
|
914
|
+
const candidates = [
|
|
915
|
+
{ key: 'output_text', allowJson: true },
|
|
916
|
+
{ key: 'text' },
|
|
917
|
+
{ key: 'content' }
|
|
918
|
+
];
|
|
919
|
+
for (const candidate of candidates) {
|
|
920
|
+
if (!(candidate.key in record)) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const text = this.normalizeTextCandidateValue(record[candidate.key], candidate.allowJson === true);
|
|
924
|
+
if (text) {
|
|
925
|
+
return text;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
normalizeTextCandidateValue(value, allowJsonStringify = false) {
|
|
931
|
+
if (!value) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
if (typeof value === 'string') {
|
|
935
|
+
const trimmed = value.trim();
|
|
936
|
+
return trimmed.length ? trimmed : null;
|
|
937
|
+
}
|
|
938
|
+
if (Array.isArray(value)) {
|
|
939
|
+
const collected = [];
|
|
940
|
+
for (const entry of value) {
|
|
941
|
+
const nested = this.normalizeTextCandidateValue(entry, allowJsonStringify);
|
|
942
|
+
if (nested) {
|
|
943
|
+
collected.push(nested);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return collected.length ? collected.join('\n') : null;
|
|
947
|
+
}
|
|
948
|
+
if (typeof value === 'object') {
|
|
949
|
+
const bag = value;
|
|
950
|
+
const textField = bag.text;
|
|
951
|
+
if (typeof textField === 'string' && textField.trim()) {
|
|
952
|
+
return textField.trim();
|
|
953
|
+
}
|
|
954
|
+
const summaryField = bag.summary;
|
|
955
|
+
if (typeof summaryField === 'string' && summaryField.trim()) {
|
|
956
|
+
return summaryField.trim();
|
|
957
|
+
}
|
|
958
|
+
if ('content' in bag) {
|
|
959
|
+
const nested = this.normalizeTextCandidateValue(bag.content, allowJsonStringify);
|
|
960
|
+
if (nested) {
|
|
961
|
+
return nested;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (allowJsonStringify) {
|
|
965
|
+
try {
|
|
966
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
967
|
+
return serialized.trim() || null;
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
582
976
|
}
|
|
583
977
|
export function createRequestExecutor(deps) {
|
|
584
978
|
return new HubRequestExecutor(deps);
|