minimal-agent 0.18.13 → 0.18.14
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/package.json +1 -3
- package/plugins/workflow-runner/src/runtime/context.js +1 -1
- package/plugins/workflow-runner/src/runtime/context.ts +1 -1
- package/plugins/workflow-runner/test/embeddedExecution.test.ts +2 -5
- package/plugins/workflow-runner/test/execLifetime.test.ts +15 -2
- package/skills/ssh-remote-dev/SKILL.md +61 -0
- package/skills/ssh-remote-dev/agents/openai.yaml +4 -0
- package/src/advisor/README.md +62 -24
- package/src/advisor/advisorNudge.js +199 -0
- package/src/advisor/advisorNudge.ts +232 -0
- package/src/advisor/deliveryReview.js +35 -12
- package/src/advisor/deliveryReview.ts +35 -11
- package/src/advisor/presentation.js +7 -10
- package/src/advisor/presentation.ts +10 -13
- package/src/advisor/reviewContext.js +22 -58
- package/src/advisor/reviewContext.ts +24 -67
- package/src/cli/configMenu.js +17 -128
- package/src/cli/configMenu.tsx +17 -193
- package/src/cli/print.js +2 -0
- package/src/cli/print.ts +2 -0
- package/src/cli/streamJson.js +2 -0
- package/src/cli/streamJson.ts +2 -0
- package/src/cli/wizards/advisorWizard.js +9 -7
- package/src/cli/wizards/advisorWizard.tsx +9 -7
- package/src/cli/wizards/contextAdvisorWizard.js +9 -7
- package/src/cli/wizards/contextAdvisorWizard.tsx +9 -7
- package/src/cli/wizards/providerPresets.js +27 -1
- package/src/cli/wizards/providerPresets.ts +44 -2
- package/src/cli/wizards/providerWizard.js +51 -10
- package/src/cli/wizards/providerWizard.tsx +59 -9
- package/src/config/configDefaults.js +1 -1
- package/src/config/configDefaults.ts +1 -1
- package/src/config/configDiagnostics.js +1 -21
- package/src/config/configDiagnostics.ts +3 -23
- package/src/config/configFile.js +11 -4
- package/src/config/configFile.ts +27 -7
- package/src/config/configManagement.js +1 -0
- package/src/config/configManagement.ts +2 -0
- package/src/config/configService.js +12 -8
- package/src/config/configService.ts +16 -11
- package/src/config/numericLimits.js +0 -4
- package/src/config/numericLimits.ts +0 -4
- package/src/config/reasoningEffort.js +16 -0
- package/src/config/reasoningEffort.ts +18 -0
- package/src/context/budgetPlan.js +1 -1
- package/src/context/budgetPlan.ts +1 -1
- package/src/context/optimizeConversation.js +1 -1
- package/src/context/optimizeConversation.ts +1 -1
- package/src/harness/stallGuard.js +8 -13
- package/src/harness/stallGuard.ts +8 -12
- package/src/llm/client.js +64 -17
- package/src/llm/client.ts +62 -7
- package/src/llm/providers/openaiFormat.js +14 -3
- package/src/llm/providers/openaiFormat.ts +17 -3
- package/src/llm/providers/responsesFormat.js +275 -0
- package/src/llm/providers/responsesFormat.ts +306 -0
- package/src/loop/README.md +18 -3
- package/src/loop/liveDiagramGate.js +63 -0
- package/src/loop/liveDiagramGate.ts +61 -0
- package/src/loop/messages.js +12 -0
- package/src/loop/messages.ts +13 -0
- package/src/loop/options.ts +7 -2
- package/src/loop/toolBatch.js +0 -1
- package/src/loop/toolBatch.ts +0 -1
- package/src/loop/toolExecutor.js +11 -46
- package/src/loop/toolExecutor.ts +12 -55
- package/src/loop/toolPresentation.js +2 -13
- package/src/loop/toolPresentation.ts +2 -13
- package/src/loop/toolSemantics.js +3 -4
- package/src/loop/toolSemantics.ts +4 -6
- package/src/loop.js +222 -214
- package/src/loop.ts +226 -232
- package/src/memory/dependencies.js +11 -3
- package/src/memory/dependencies.ts +11 -3
- package/src/memory/episodicMemory.js +59 -18
- package/src/memory/episodicMemory.ts +58 -18
- package/src/memory/experienceStore.js +4 -3
- package/src/memory/experienceStore.ts +4 -3
- package/src/memory/factCandidates.js +53 -1
- package/src/memory/factCandidates.ts +54 -1
- package/src/memory/factLearning.js +2 -2
- package/src/memory/factLearning.ts +2 -2
- package/src/memory/jobPolicy.js +24 -5
- package/src/memory/jobPolicy.ts +25 -5
- package/src/memory/learning.js +2 -2
- package/src/memory/learning.ts +2 -2
- package/src/memory/learningModel.js +7 -0
- package/src/memory/learningModel.ts +7 -0
- package/src/memory/managementService.js +8 -7
- package/src/memory/managementService.ts +16 -8
- package/src/memory/memoryService.js +104 -33
- package/src/memory/memoryService.ts +132 -28
- package/src/memory/projectKnowledge.js +39 -3
- package/src/memory/projectKnowledge.ts +45 -3
- package/src/memory/recallQuery.js +56 -0
- package/src/memory/recallQuery.ts +58 -0
- package/src/memory/retrieval.js +39 -10
- package/src/memory/retrieval.ts +46 -11
- package/src/memory/turnJournal.js +75 -10
- package/src/memory/turnJournal.ts +66 -10
- package/src/memory/types.ts +7 -1
- package/src/prompts/delegation.js +2 -1
- package/src/prompts/delegation.ts +3 -1
- package/src/prompts/policyRegistry.js +9 -12
- package/src/prompts/policyRegistry.ts +9 -12
- package/src/prompts/promptAssembly.js +6 -5
- package/src/prompts/promptAssembly.ts +8 -6
- package/src/prompts/visualPresentation.js +39 -10
- package/src/prompts/visualPresentation.ts +38 -9
- package/src/runtime/answerPreparation.js +1 -1
- package/src/runtime/answerPreparation.ts +1 -1
- package/src/runtime/fileLock.js +19 -3
- package/src/runtime/fileLock.ts +19 -3
- package/src/runtime/invocationPolicy.js +1 -1
- package/src/runtime/invocationPolicy.ts +1 -1
- package/src/runtime/outputContract.js +0 -4
- package/src/runtime/outputContract.ts +0 -7
- package/src/session/runAgentTurn.js +3 -0
- package/src/session/runAgentTurn.ts +3 -0
- package/src/team/onlineToolContract.js +0 -1
- package/src/team/onlineToolContract.ts +0 -1
- package/src/tools/advisor/advisor.js +207 -365
- package/src/tools/advisor/advisor.ts +211 -383
- package/src/tools/agent/agentTypes.js +0 -1
- package/src/tools/agent/agentTypes.ts +0 -1
- package/src/tools/agent/coordinator.js +4 -1
- package/src/tools/agent/coordinator.ts +4 -1
- package/src/tools/bash/semantics.js +4 -5
- package/src/tools/bash/semantics.ts +4 -5
- package/src/tools/capabilities.js +2 -3
- package/src/tools/capabilities.ts +2 -4
- package/src/tools/index.js +0 -16
- package/src/tools/index.ts +0 -16
- package/src/tools/memory/maintenance.js +8 -9
- package/src/tools/memory/maintenance.ts +13 -11
- package/src/tools/memory/memory.js +5 -1
- package/src/tools/memory/memory.ts +5 -0
- package/src/tools/optimizeConversation/README.md +4 -6
- package/src/tools/toolPromptGuides.js +2 -9
- package/src/tools/toolPromptGuides.ts +2 -9
- package/src/types.ts +28 -105
- package/src/ui/App.js +2 -0
- package/src/ui/App.tsx +1 -0
- package/src/ui/MessageList.js +8 -44
- package/src/ui/MessageList.tsx +3 -81
- package/src/ui/hooks/useChat.js +2 -0
- package/src/ui/hooks/useChat.ts +2 -0
- package/src/ui/toolProgress.js +1 -1
- package/src/ui/toolProgress.ts +1 -1
- package/src/web/protocol.ts +10 -3
- package/src/advisor/advisorCoordinator.js +0 -734
- package/src/advisor/advisorCoordinator.ts +0 -825
- package/src/cli/wizards/sshHostWizard.js +0 -156
- package/src/cli/wizards/sshHostWizard.tsx +0 -214
- package/src/context/sshHosts.js +0 -290
- package/src/context/sshHosts.ts +0 -312
- package/src/loop/publication.js +0 -94
- package/src/loop/publication.ts +0 -124
- package/src/runtime/finalAnswer.js +0 -9
- package/src/runtime/finalAnswer.ts +0 -13
- package/src/tools/advisor/advisorDecision.js +0 -79
- package/src/tools/advisor/advisorDecision.ts +0 -92
- package/src/tools/ssh/ssh.js +0 -152
- package/src/tools/ssh/ssh.ts +0 -167
- package/src/tools/ssh/sshAvailability.js +0 -35
- package/src/tools/ssh/sshAvailability.ts +0 -39
- package/src/tools/ssh/sshClient.js +0 -177
- package/src/tools/ssh/sshClient.ts +0 -189
- package/src/tools/ssh/sshDownload.js +0 -77
- package/src/tools/ssh/sshDownload.ts +0 -92
- package/src/tools/ssh/sshHostsTool.js +0 -36
- package/src/tools/ssh/sshHostsTool.ts +0 -48
- package/src/tools/ssh/sshUpload.js +0 -72
- package/src/tools/ssh/sshUpload.ts +0 -85
package/src/llm/client.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { dlog } from '../utils/debugLog.js';
|
|
27
27
|
import { buildOpenAIRequest, createOpenAISSEParser, isImageInputRejected, isMaxTokensRejected, isPromptCacheKeyRejected, isResponseFormatUnsupported, } from './providers/openaiFormat.js';
|
|
28
28
|
import { promptMaterialHash, shortCacheScope } from './promptCache.js';
|
|
29
|
+
import { buildResponsesRequest, createResponsesSSEParser, isReasoningEffortRejected, } from './providers/responsesFormat.js';
|
|
29
30
|
import { buildAnthropicRequest, createAnthropicSSEParser, } from './providers/anthropicFormat.js';
|
|
30
31
|
import { chatGateway } from './gatewayClient.js';
|
|
31
32
|
import { prepareMessagesForProvider } from './messageProvenance.js';
|
|
@@ -42,6 +43,11 @@ const rfUnsupported = new Set();
|
|
|
42
43
|
*/
|
|
43
44
|
const maxTokensUnsupported = new Set();
|
|
44
45
|
const promptCacheKeyUnsupported = new Set();
|
|
46
|
+
/**
|
|
47
|
+
* 记住"这个 baseURL::model 不认我们配的推理强度"。强度档位因模型而异(有的没有 none/minimal,
|
|
48
|
+
* 非推理模型根本没有 reasoning 参数),拒绝时去掉该参数重发一次,而不是让整个对话 400。
|
|
49
|
+
*/
|
|
50
|
+
const reasoningEffortUnsupported = new Set();
|
|
45
51
|
/** 调试日志用的请求自增号(vote 并发多 voter 时靠它在日志里区分是哪一路)。 */
|
|
46
52
|
let reqCounter = 0;
|
|
47
53
|
// ============================================================
|
|
@@ -278,7 +284,11 @@ async function* streamLocal(args) {
|
|
|
278
284
|
const maxAttempts = args.maxAttempts ?? DEFAULT_RETRY.maxAttempts;
|
|
279
285
|
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1)
|
|
280
286
|
throw new Error('Invalid maxAttempts');
|
|
281
|
-
const format = provider.apiFormat === 'anthropic'
|
|
287
|
+
const format = provider.apiFormat === 'anthropic'
|
|
288
|
+
? 'anthropic'
|
|
289
|
+
: provider.apiFormat === 'openai-responses'
|
|
290
|
+
? 'openai-responses'
|
|
291
|
+
: 'openai';
|
|
282
292
|
if (format === 'anthropic' && responseFormat) {
|
|
283
293
|
throw new Error('output_schema 结构化输出目前不支持 Anthropic Messages 格式的 provider' +
|
|
284
294
|
'(Anthropic 的强制工具输出模式与 OpenAI 的 response_format.json_schema 形状不同,尚未实现映射)。' +
|
|
@@ -292,15 +302,18 @@ async function* streamLocal(args) {
|
|
|
292
302
|
const rfKey = `${provider.baseURL}::${provider.model}`;
|
|
293
303
|
// OpenAI 分支专属的「就地退化」状态;Anthropic 无此概念,两个 flag 恒为 false,
|
|
294
304
|
// 下面的退化 if 分支天然不会进入(不需要额外的 format 判断散落各处)。
|
|
295
|
-
let wantResponseFormat = format
|
|
305
|
+
let wantResponseFormat = format !== 'anthropic' &&
|
|
296
306
|
responseFormat !== undefined &&
|
|
297
307
|
!rfUnsupported.has(rfKey);
|
|
298
|
-
let wantMaxTokens = format
|
|
308
|
+
let wantMaxTokens = format !== 'anthropic' &&
|
|
299
309
|
typeof provider.maxTokens === 'number' &&
|
|
300
310
|
provider.maxTokens > 0 &&
|
|
301
311
|
!maxTokensUnsupported.has(rfKey);
|
|
302
312
|
let imagesStripped = false;
|
|
303
|
-
let
|
|
313
|
+
let wantReasoningEffort = format !== 'anthropic' &&
|
|
314
|
+
!!provider.reasoningEffort &&
|
|
315
|
+
!reasoningEffortUnsupported.has(rfKey);
|
|
316
|
+
let wantPromptCacheKey = format !== 'anthropic' &&
|
|
304
317
|
provider.promptCache?.mode === 'session-key' &&
|
|
305
318
|
!!promptCache?.scopeKey &&
|
|
306
319
|
!promptCacheKeyUnsupported.has(rfKey);
|
|
@@ -330,18 +343,31 @@ async function* streamLocal(args) {
|
|
|
330
343
|
tools,
|
|
331
344
|
...(cacheEdits ? { cacheEdits } : {}),
|
|
332
345
|
})
|
|
333
|
-
:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
346
|
+
: format === 'openai-responses'
|
|
347
|
+
? buildResponsesRequest({
|
|
348
|
+
provider,
|
|
349
|
+
cleanedMessages,
|
|
350
|
+
tools,
|
|
351
|
+
withMaxTokens: wantMaxTokens,
|
|
352
|
+
withReasoningEffort: wantReasoningEffort,
|
|
353
|
+
withPromptCacheKey: wantPromptCacheKey,
|
|
354
|
+
promptCache,
|
|
355
|
+
responseFormat,
|
|
356
|
+
withResponseFormat: wantResponseFormat,
|
|
357
|
+
})
|
|
358
|
+
: buildOpenAIRequest({
|
|
359
|
+
provider,
|
|
360
|
+
cleanedMessages,
|
|
361
|
+
tools,
|
|
362
|
+
withResponseFormat: wantResponseFormat,
|
|
363
|
+
withMaxTokens: wantMaxTokens,
|
|
364
|
+
includeUsage,
|
|
365
|
+
promptCache,
|
|
366
|
+
withPromptCacheKey: wantPromptCacheKey,
|
|
367
|
+
responseFormat,
|
|
368
|
+
reasoning: provider.reasoning,
|
|
369
|
+
withReasoningEffort: wantReasoningEffort,
|
|
370
|
+
});
|
|
345
371
|
return fetch(req.url, {
|
|
346
372
|
method: 'POST',
|
|
347
373
|
headers: req.headers,
|
|
@@ -440,6 +466,25 @@ async function* streamLocal(args) {
|
|
|
440
466
|
break;
|
|
441
467
|
errText = await resp.text().catch(() => '');
|
|
442
468
|
}
|
|
469
|
+
// 推理强度不被该模型接受 → 去掉 reasoning 参数重发一次(沿用服务端默认强度),并告警。
|
|
470
|
+
if (wantReasoningEffort && isReasoningEffortRejected(resp.status, errText)) {
|
|
471
|
+
reasoningEffortUnsupported.add(rfKey);
|
|
472
|
+
wantReasoningEffort = false;
|
|
473
|
+
console.warn(`[minimal-agent] provider model "${provider.model}" 不接受推理强度 "${provider.reasoningEffort}",` +
|
|
474
|
+
`已去掉该参数重发(沿用服务端默认强度)。可在 /config 里改选该模型支持的档位。`);
|
|
475
|
+
try {
|
|
476
|
+
resp = await postChat();
|
|
477
|
+
watchdog.kick();
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
if ((await handleNetworkError(e, attempt, 'reasoning-effort fallback fetch failed')) === 'break')
|
|
481
|
+
break;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (resp.ok)
|
|
485
|
+
break;
|
|
486
|
+
errText = await resp.text().catch(() => '');
|
|
487
|
+
}
|
|
443
488
|
// 退化①:response_format.json_schema 不支持
|
|
444
489
|
if (wantResponseFormat &&
|
|
445
490
|
isResponseFormatUnsupported(resp.status, errText)) {
|
|
@@ -554,7 +599,9 @@ async function* streamLocal(args) {
|
|
|
554
599
|
let buffer = '';
|
|
555
600
|
const parser = format === 'anthropic'
|
|
556
601
|
? createAnthropicSSEParser()
|
|
557
|
-
:
|
|
602
|
+
: format === 'openai-responses'
|
|
603
|
+
? createResponsesSSEParser()
|
|
604
|
+
: createOpenAISSEParser();
|
|
558
605
|
try {
|
|
559
606
|
while (true) {
|
|
560
607
|
// 每次 read 前检查 abort:用户按 ESC/Ctrl+C 时 signal.aborted 为 true。
|
package/src/llm/client.ts
CHANGED
|
@@ -42,6 +42,11 @@ import {
|
|
|
42
42
|
isResponseFormatUnsupported,
|
|
43
43
|
} from './providers/openaiFormat.js';
|
|
44
44
|
import { promptMaterialHash, shortCacheScope } from './promptCache.js';
|
|
45
|
+
import {
|
|
46
|
+
buildResponsesRequest,
|
|
47
|
+
createResponsesSSEParser,
|
|
48
|
+
isReasoningEffortRejected,
|
|
49
|
+
} from './providers/responsesFormat.js';
|
|
45
50
|
import {
|
|
46
51
|
buildAnthropicRequest,
|
|
47
52
|
createAnthropicSSEParser,
|
|
@@ -64,6 +69,11 @@ const rfUnsupported = new Set<string>();
|
|
|
64
69
|
*/
|
|
65
70
|
const maxTokensUnsupported = new Set<string>();
|
|
66
71
|
const promptCacheKeyUnsupported = new Set<string>();
|
|
72
|
+
/**
|
|
73
|
+
* 记住"这个 baseURL::model 不认我们配的推理强度"。强度档位因模型而异(有的没有 none/minimal,
|
|
74
|
+
* 非推理模型根本没有 reasoning 参数),拒绝时去掉该参数重发一次,而不是让整个对话 400。
|
|
75
|
+
*/
|
|
76
|
+
const reasoningEffortUnsupported = new Set<string>();
|
|
67
77
|
|
|
68
78
|
/** 调试日志用的请求自增号(vote 并发多 voter 时靠它在日志里区分是哪一路)。 */
|
|
69
79
|
let reqCounter = 0;
|
|
@@ -385,8 +395,12 @@ async function* streamLocal(args: {
|
|
|
385
395
|
} = args;
|
|
386
396
|
const maxAttempts = args.maxAttempts ?? DEFAULT_RETRY.maxAttempts;
|
|
387
397
|
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) throw new Error('Invalid maxAttempts');
|
|
388
|
-
const format: 'openai' | 'anthropic' =
|
|
389
|
-
provider.apiFormat === 'anthropic'
|
|
398
|
+
const format: 'openai' | 'openai-responses' | 'anthropic' =
|
|
399
|
+
provider.apiFormat === 'anthropic'
|
|
400
|
+
? 'anthropic'
|
|
401
|
+
: provider.apiFormat === 'openai-responses'
|
|
402
|
+
? 'openai-responses'
|
|
403
|
+
: 'openai';
|
|
390
404
|
|
|
391
405
|
if (format === 'anthropic' && responseFormat) {
|
|
392
406
|
throw new Error(
|
|
@@ -410,17 +424,21 @@ async function* streamLocal(args: {
|
|
|
410
424
|
// OpenAI 分支专属的「就地退化」状态;Anthropic 无此概念,两个 flag 恒为 false,
|
|
411
425
|
// 下面的退化 if 分支天然不会进入(不需要额外的 format 判断散落各处)。
|
|
412
426
|
let wantResponseFormat =
|
|
413
|
-
format
|
|
427
|
+
format !== 'anthropic' &&
|
|
414
428
|
responseFormat !== undefined &&
|
|
415
429
|
!rfUnsupported.has(rfKey);
|
|
416
430
|
let wantMaxTokens =
|
|
417
|
-
format
|
|
431
|
+
format !== 'anthropic' &&
|
|
418
432
|
typeof provider.maxTokens === 'number' &&
|
|
419
433
|
provider.maxTokens > 0 &&
|
|
420
434
|
!maxTokensUnsupported.has(rfKey);
|
|
421
435
|
let imagesStripped = false;
|
|
436
|
+
let wantReasoningEffort =
|
|
437
|
+
format !== 'anthropic' &&
|
|
438
|
+
!!provider.reasoningEffort &&
|
|
439
|
+
!reasoningEffortUnsupported.has(rfKey);
|
|
422
440
|
let wantPromptCacheKey =
|
|
423
|
-
format
|
|
441
|
+
format !== 'anthropic' &&
|
|
424
442
|
provider.promptCache?.mode === 'session-key' &&
|
|
425
443
|
!!promptCache?.scopeKey &&
|
|
426
444
|
!promptCacheKeyUnsupported.has(rfKey);
|
|
@@ -455,7 +473,19 @@ async function* streamLocal(args: {
|
|
|
455
473
|
tools,
|
|
456
474
|
...(cacheEdits ? { cacheEdits } : {}),
|
|
457
475
|
})
|
|
458
|
-
:
|
|
476
|
+
: format === 'openai-responses'
|
|
477
|
+
? buildResponsesRequest({
|
|
478
|
+
provider,
|
|
479
|
+
cleanedMessages,
|
|
480
|
+
tools,
|
|
481
|
+
withMaxTokens: wantMaxTokens,
|
|
482
|
+
withReasoningEffort: wantReasoningEffort,
|
|
483
|
+
withPromptCacheKey: wantPromptCacheKey,
|
|
484
|
+
promptCache,
|
|
485
|
+
responseFormat,
|
|
486
|
+
withResponseFormat: wantResponseFormat,
|
|
487
|
+
})
|
|
488
|
+
: buildOpenAIRequest({
|
|
459
489
|
provider,
|
|
460
490
|
cleanedMessages,
|
|
461
491
|
tools,
|
|
@@ -466,6 +496,7 @@ async function* streamLocal(args: {
|
|
|
466
496
|
withPromptCacheKey: wantPromptCacheKey,
|
|
467
497
|
responseFormat,
|
|
468
498
|
reasoning: provider.reasoning,
|
|
499
|
+
withReasoningEffort: wantReasoningEffort,
|
|
469
500
|
});
|
|
470
501
|
return fetch(req.url, {
|
|
471
502
|
method: 'POST',
|
|
@@ -590,6 +621,28 @@ async function* streamLocal(args: {
|
|
|
590
621
|
errText = await resp.text().catch(() => '');
|
|
591
622
|
}
|
|
592
623
|
|
|
624
|
+
// 推理强度不被该模型接受 → 去掉 reasoning 参数重发一次(沿用服务端默认强度),并告警。
|
|
625
|
+
if (wantReasoningEffort && isReasoningEffortRejected(resp.status, errText)) {
|
|
626
|
+
reasoningEffortUnsupported.add(rfKey);
|
|
627
|
+
wantReasoningEffort = false;
|
|
628
|
+
console.warn(
|
|
629
|
+
`[minimal-agent] provider model "${provider.model}" 不接受推理强度 "${provider.reasoningEffort}",` +
|
|
630
|
+
`已去掉该参数重发(沿用服务端默认强度)。可在 /config 里改选该模型支持的档位。`,
|
|
631
|
+
);
|
|
632
|
+
try {
|
|
633
|
+
resp = await postChat();
|
|
634
|
+
watchdog.kick();
|
|
635
|
+
} catch (e) {
|
|
636
|
+
if (
|
|
637
|
+
(await handleNetworkError(e, attempt, 'reasoning-effort fallback fetch failed')) === 'break'
|
|
638
|
+
)
|
|
639
|
+
break;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (resp.ok) break;
|
|
643
|
+
errText = await resp.text().catch(() => '');
|
|
644
|
+
}
|
|
645
|
+
|
|
593
646
|
// 退化①:response_format.json_schema 不支持
|
|
594
647
|
if (
|
|
595
648
|
wantResponseFormat &&
|
|
@@ -730,7 +783,9 @@ async function* streamLocal(args: {
|
|
|
730
783
|
const parser =
|
|
731
784
|
format === 'anthropic'
|
|
732
785
|
? createAnthropicSSEParser()
|
|
733
|
-
:
|
|
786
|
+
: format === 'openai-responses'
|
|
787
|
+
? createResponsesSSEParser()
|
|
788
|
+
: createOpenAISSEParser();
|
|
734
789
|
|
|
735
790
|
try {
|
|
736
791
|
while (true) {
|
|
@@ -15,7 +15,8 @@ export function isResponseFormatUnsupported(status, errText) {
|
|
|
15
15
|
if (status !== 400)
|
|
16
16
|
return false;
|
|
17
17
|
const t = errText.toLowerCase();
|
|
18
|
-
|
|
18
|
+
// Responses API 把它叫 text.format。
|
|
19
|
+
if (!t.includes('response_format') && !t.includes('text.format'))
|
|
19
20
|
return false;
|
|
20
21
|
return (t.includes('not support') ||
|
|
21
22
|
t.includes('unsupported') ||
|
|
@@ -31,7 +32,7 @@ export function isMaxTokensRejected(status, errText) {
|
|
|
31
32
|
if (status !== 400)
|
|
32
33
|
return false;
|
|
33
34
|
const t = errText.toLowerCase();
|
|
34
|
-
if (!t.includes('max_tokens') && !t.includes('max tokens'))
|
|
35
|
+
if (!t.includes('max_tokens') && !t.includes('max tokens') && !t.includes('max_output_tokens'))
|
|
35
36
|
return false;
|
|
36
37
|
return (t.includes('not support') ||
|
|
37
38
|
t.includes('unsupported') ||
|
|
@@ -189,6 +190,9 @@ export function buildOpenAIUrl(provider) {
|
|
|
189
190
|
}
|
|
190
191
|
return url;
|
|
191
192
|
}
|
|
193
|
+
function isOpenRouter(baseURL) {
|
|
194
|
+
return /(?:^|\/\/|\.)openrouter\.ai(?:[/:]|$)/i.test(baseURL);
|
|
195
|
+
}
|
|
192
196
|
export function buildOpenAIRequest(args) {
|
|
193
197
|
const { provider, cleanedMessages, tools, withResponseFormat, withMaxTokens, includeUsage, responseFormat, promptCache, withPromptCacheKey, } = args;
|
|
194
198
|
// 转换 image_url.detail: 'default' → 'auto',兼容所有 OpenAI 兼容 provider
|
|
@@ -224,7 +228,14 @@ export function buildOpenAIRequest(args) {
|
|
|
224
228
|
// OpenRouter and other compatible providers accept this optional extension.
|
|
225
229
|
// Omit it unless explicitly enabled so ordinary OpenAI-compatible endpoints
|
|
226
230
|
// retain their default reasoning behavior.
|
|
227
|
-
|
|
231
|
+
// reasoningEffort 优先:OpenRouter 认 reasoning.effort,OpenAI 官方 Chat Completions
|
|
232
|
+
// 认顶层 reasoning_effort,二者按端点分开发,避免把 OpenRouter 形状发给官方端点。
|
|
233
|
+
reasoning: args.withReasoningEffort && provider.reasoningEffort && isOpenRouter(provider.baseURL)
|
|
234
|
+
? { effort: provider.reasoningEffort }
|
|
235
|
+
: args.reasoning ? { enabled: true } : undefined,
|
|
236
|
+
reasoning_effort: args.withReasoningEffort && provider.reasoningEffort && !isOpenRouter(provider.baseURL)
|
|
237
|
+
? provider.reasoningEffort
|
|
238
|
+
: undefined,
|
|
228
239
|
// ADR-05: structured output;缺省 undefined 不出现在 body(JSON.stringify 自动剥离)
|
|
229
240
|
response_format: withResponseFormat ? responseFormat : undefined,
|
|
230
241
|
...(withPromptCacheKey && promptCache?.scopeKey
|
|
@@ -35,7 +35,8 @@ interface OpenAIToolSpec {
|
|
|
35
35
|
export function isResponseFormatUnsupported(status: number, errText: string): boolean {
|
|
36
36
|
if (status !== 400) return false;
|
|
37
37
|
const t = errText.toLowerCase();
|
|
38
|
-
|
|
38
|
+
// Responses API 把它叫 text.format。
|
|
39
|
+
if (!t.includes('response_format') && !t.includes('text.format')) return false;
|
|
39
40
|
return (
|
|
40
41
|
t.includes('not support') ||
|
|
41
42
|
t.includes('unsupported') ||
|
|
@@ -52,7 +53,7 @@ export function isResponseFormatUnsupported(status: number, errText: string): bo
|
|
|
52
53
|
export function isMaxTokensRejected(status: number, errText: string): boolean {
|
|
53
54
|
if (status !== 400) return false;
|
|
54
55
|
const t = errText.toLowerCase();
|
|
55
|
-
if (!t.includes('max_tokens') && !t.includes('max tokens')) return false;
|
|
56
|
+
if (!t.includes('max_tokens') && !t.includes('max tokens') && !t.includes('max_output_tokens')) return false;
|
|
56
57
|
return (
|
|
57
58
|
t.includes('not support') ||
|
|
58
59
|
t.includes('unsupported') ||
|
|
@@ -121,6 +122,8 @@ export interface OpenAIRequestArgs {
|
|
|
121
122
|
promptCache?: PromptCacheContext;
|
|
122
123
|
withPromptCacheKey?: boolean;
|
|
123
124
|
reasoning?: boolean;
|
|
125
|
+
/** 发送 provider.reasoningEffort(被服务端拒绝过的模型由 client.ts 关掉)。 */
|
|
126
|
+
withReasoningEffort?: boolean;
|
|
124
127
|
responseFormat?: {
|
|
125
128
|
type: 'json_schema';
|
|
126
129
|
json_schema: { name: string; schema: Record<string, unknown>; strict?: boolean };
|
|
@@ -236,6 +239,10 @@ export function buildOpenAIUrl(provider: LocalProvider): string {
|
|
|
236
239
|
return url;
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
function isOpenRouter(baseURL: string): boolean {
|
|
243
|
+
return /(?:^|\/\/|\.)openrouter\.ai(?:[/:]|$)/i.test(baseURL);
|
|
244
|
+
}
|
|
245
|
+
|
|
239
246
|
export function buildOpenAIRequest(args: OpenAIRequestArgs): { url: string; headers: Record<string, string>; body: string } {
|
|
240
247
|
const {
|
|
241
248
|
provider,
|
|
@@ -281,7 +288,14 @@ export function buildOpenAIRequest(args: OpenAIRequestArgs): { url: string; head
|
|
|
281
288
|
// OpenRouter and other compatible providers accept this optional extension.
|
|
282
289
|
// Omit it unless explicitly enabled so ordinary OpenAI-compatible endpoints
|
|
283
290
|
// retain their default reasoning behavior.
|
|
284
|
-
|
|
291
|
+
// reasoningEffort 优先:OpenRouter 认 reasoning.effort,OpenAI 官方 Chat Completions
|
|
292
|
+
// 认顶层 reasoning_effort,二者按端点分开发,避免把 OpenRouter 形状发给官方端点。
|
|
293
|
+
reasoning: args.withReasoningEffort && provider.reasoningEffort && isOpenRouter(provider.baseURL)
|
|
294
|
+
? { effort: provider.reasoningEffort }
|
|
295
|
+
: args.reasoning ? { enabled: true } : undefined,
|
|
296
|
+
reasoning_effort: args.withReasoningEffort && provider.reasoningEffort && !isOpenRouter(provider.baseURL)
|
|
297
|
+
? provider.reasoningEffort
|
|
298
|
+
: undefined,
|
|
285
299
|
// ADR-05: structured output;缺省 undefined 不出现在 body(JSON.stringify 自动剥离)
|
|
286
300
|
response_format: withResponseFormat ? responseFormat : undefined,
|
|
287
301
|
...(withPromptCacheKey && promptCache?.scopeKey
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* src/llm/providers/responsesFormat.ts —— OpenAI Responses API 协议
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* ⚠️ 未经真实端点验证:本机没有可用于付费调用的 OpenAI key,请求体与 SSE 事件字段
|
|
6
|
+
* 照官方文档(POST /v1/responses,stream:true)实现,配了手写的 SSE 录像单测,
|
|
7
|
+
* 没有做过真实 round-trip。拿到 key 后先跑一次真实对话冒烟(含一次工具调用)。
|
|
8
|
+
*
|
|
9
|
+
* 与 openaiFormat.ts / anthropicFormat.ts 同一分工:这里只做「内部 Message[]/Tool[]
|
|
10
|
+
* ⇄ Responses 协议形状」的翻译 + SSE 逐帧解析;重试/退避/空闲看门狗/就地退化
|
|
11
|
+
* 留在 client.ts。
|
|
12
|
+
*
|
|
13
|
+
* 设计取舍(都是为了让它在不依赖服务端会话状态的前提下正确):
|
|
14
|
+
* - 无状态:`store:false`,每次请求重发完整历史,不用 previous_response_id。
|
|
15
|
+
* 与本项目「history 是唯一事实源、压缩/快照在本地」的前提一致。
|
|
16
|
+
* - 工具 `strict:false`:Responses 的函数工具默认 strict:true,要求
|
|
17
|
+
* additionalProperties:false 且全字段 required;本项目的工具 schema 不满足,
|
|
18
|
+
* 不显式关掉就会整批 400。
|
|
19
|
+
* - 回传的 function_call 只带 call_id/name/arguments,**不带 id**:带上 fc_ id
|
|
20
|
+
* 而缺少配对的 reasoning 项,服务端会报错;无状态重发不保存 reasoning 项,
|
|
21
|
+
* 所以只能不带。代价是推理模型在多轮工具调用间丢失上一轮的推理上下文
|
|
22
|
+
* (质量略降,不影响正确性)。
|
|
23
|
+
* - 不请求 reasoning.summary:部分组织需要先做身份验证才能拿推理摘要,
|
|
24
|
+
* 请求了反而 400。服务端若主动发摘要增量,照常显示为思维链。
|
|
25
|
+
* ============================================================
|
|
26
|
+
*/
|
|
27
|
+
function userContent(content) {
|
|
28
|
+
if (typeof content === 'string')
|
|
29
|
+
return content;
|
|
30
|
+
return content.map((part) => {
|
|
31
|
+
if (part.type === 'text')
|
|
32
|
+
return { type: 'input_text', text: part.text };
|
|
33
|
+
if (part.type === 'image_url') {
|
|
34
|
+
const detail = part.image_url.detail;
|
|
35
|
+
return {
|
|
36
|
+
type: 'input_image',
|
|
37
|
+
image_url: part.image_url.url,
|
|
38
|
+
// 'default' 是本项目内部取值,Responses 只认 auto/low/high。
|
|
39
|
+
detail: detail === 'low' || detail === 'high' ? detail : 'auto',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { type: 'input_text', text: '[视频内容未发送:Responses 格式的 provider 暂不支持视频输入]' };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** 内部 Message[](OpenAI Chat 形状)→ Responses 的 input 数组。 */
|
|
46
|
+
export function toResponsesInput(messages) {
|
|
47
|
+
const input = [];
|
|
48
|
+
for (const message of messages) {
|
|
49
|
+
if (message.role === 'system') {
|
|
50
|
+
input.push({ role: 'system', content: message.content });
|
|
51
|
+
}
|
|
52
|
+
else if (message.role === 'user') {
|
|
53
|
+
input.push({ role: 'user', content: userContent(message.content) });
|
|
54
|
+
}
|
|
55
|
+
else if (message.role === 'assistant') {
|
|
56
|
+
if (message.content) {
|
|
57
|
+
input.push({ role: 'assistant', content: [{ type: 'output_text', text: message.content }] });
|
|
58
|
+
}
|
|
59
|
+
for (const call of message.tool_calls ?? []) {
|
|
60
|
+
input.push({
|
|
61
|
+
type: 'function_call',
|
|
62
|
+
call_id: call.id,
|
|
63
|
+
name: call.function.name,
|
|
64
|
+
arguments: call.function.arguments || '{}',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
input.push({ type: 'function_call_output', call_id: message.tool_call_id, output: message.content });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return input;
|
|
73
|
+
}
|
|
74
|
+
function looksLikeVersionedBase(url) {
|
|
75
|
+
try {
|
|
76
|
+
const path = new URL(url).pathname.replace(/\/+$/, '');
|
|
77
|
+
if (path === '' || path === '/')
|
|
78
|
+
return true;
|
|
79
|
+
return /^v\d+/.test(path.split('/').at(-1));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** baseURL 是完整端点(以 /responses 结尾);只有「版本化基础路径」才自动补后缀。 */
|
|
86
|
+
export function buildResponsesUrl(provider) {
|
|
87
|
+
const url = provider.baseURL.trim().replace(/\/+$/, '');
|
|
88
|
+
if (!url || url.endsWith('/responses'))
|
|
89
|
+
return url;
|
|
90
|
+
return looksLikeVersionedBase(url) ? `${url}/responses` : url;
|
|
91
|
+
}
|
|
92
|
+
export function buildResponsesRequest(args) {
|
|
93
|
+
const { provider, cleanedMessages, tools } = args;
|
|
94
|
+
const functionTools = tools.map((tool) => ({
|
|
95
|
+
type: 'function',
|
|
96
|
+
name: tool.name,
|
|
97
|
+
description: typeof tool.description === 'function' ? tool.description() : tool.description,
|
|
98
|
+
parameters: tool.parameters,
|
|
99
|
+
strict: false,
|
|
100
|
+
}));
|
|
101
|
+
const body = {
|
|
102
|
+
model: provider.model,
|
|
103
|
+
input: toResponsesInput(cleanedMessages),
|
|
104
|
+
stream: true,
|
|
105
|
+
store: false,
|
|
106
|
+
};
|
|
107
|
+
if (functionTools.length > 0) {
|
|
108
|
+
body.tools = functionTools;
|
|
109
|
+
body.tool_choice = 'auto';
|
|
110
|
+
}
|
|
111
|
+
if (args.withMaxTokens && typeof provider.maxTokens === 'number' && provider.maxTokens > 0) {
|
|
112
|
+
body.max_output_tokens = provider.maxTokens;
|
|
113
|
+
}
|
|
114
|
+
if (args.withReasoningEffort && provider.reasoningEffort) {
|
|
115
|
+
body.reasoning = { effort: provider.reasoningEffort };
|
|
116
|
+
}
|
|
117
|
+
if (args.withResponseFormat && args.responseFormat) {
|
|
118
|
+
const schema = args.responseFormat.json_schema;
|
|
119
|
+
body.text = {
|
|
120
|
+
format: {
|
|
121
|
+
type: 'json_schema',
|
|
122
|
+
name: schema.name,
|
|
123
|
+
schema: schema.schema,
|
|
124
|
+
...(schema.strict !== undefined ? { strict: schema.strict } : {}),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (args.withPromptCacheKey && args.promptCache?.scopeKey) {
|
|
129
|
+
body.prompt_cache_key = args.promptCache.scopeKey;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
url: buildResponsesUrl(provider),
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
Authorization: `Bearer ${provider.apiKey}`,
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(body),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** 400 是否在抱怨 reasoning / effort 取值(模型不支持该档位,或根本不是推理模型)。 */
|
|
141
|
+
export function isReasoningEffortRejected(status, errText) {
|
|
142
|
+
if (status !== 400 && status !== 422)
|
|
143
|
+
return false;
|
|
144
|
+
const text = errText.toLowerCase();
|
|
145
|
+
if (!text.includes('reasoning'))
|
|
146
|
+
return false;
|
|
147
|
+
return (text.includes('not support') ||
|
|
148
|
+
text.includes('unsupported') ||
|
|
149
|
+
text.includes('invalid') ||
|
|
150
|
+
text.includes('not valid') ||
|
|
151
|
+
text.includes('unknown') ||
|
|
152
|
+
text.includes('unrecognized') ||
|
|
153
|
+
text.includes('unexpected') ||
|
|
154
|
+
text.includes('not allowed') ||
|
|
155
|
+
text.includes('supported values'));
|
|
156
|
+
}
|
|
157
|
+
function finiteNonNegative(value) {
|
|
158
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
159
|
+
}
|
|
160
|
+
/** 每个请求一个新实例;把 Responses 的 SSE `data:` 载荷翻成 LlmStreamEvent。 */
|
|
161
|
+
export function createResponsesSSEParser() {
|
|
162
|
+
/** output_index → 本轮第几个工具调用(loop 按 index 合并分片)。 */
|
|
163
|
+
const toolIndex = new Map();
|
|
164
|
+
const streamedArguments = new Set();
|
|
165
|
+
let stopReason = 'unknown';
|
|
166
|
+
let sawToolCall = false;
|
|
167
|
+
let promptTokens;
|
|
168
|
+
let outputTokens;
|
|
169
|
+
let cachedTokens;
|
|
170
|
+
let promptCacheUsage = { reported: false };
|
|
171
|
+
const readUsage = (usage) => {
|
|
172
|
+
if (!usage || typeof usage !== 'object')
|
|
173
|
+
return;
|
|
174
|
+
const u = usage;
|
|
175
|
+
// input_tokens 已含命中缓存的部分(与 Chat Completions 的 prompt_tokens 同语义)。
|
|
176
|
+
promptTokens = finiteNonNegative(u.input_tokens) ?? promptTokens;
|
|
177
|
+
outputTokens = finiteNonNegative(u.output_tokens) ?? outputTokens;
|
|
178
|
+
const details = u.input_tokens_details;
|
|
179
|
+
if (details && typeof details === 'object') {
|
|
180
|
+
cachedTokens = finiteNonNegative(details.cached_tokens) ?? cachedTokens;
|
|
181
|
+
}
|
|
182
|
+
promptCacheUsage = {
|
|
183
|
+
reported: cachedTokens !== undefined,
|
|
184
|
+
...(promptTokens !== undefined ? { promptTokens } : {}),
|
|
185
|
+
...(cachedTokens !== undefined ? { cacheReadTokens: cachedTokens } : {}),
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
const done = () => ({
|
|
189
|
+
type: 'done',
|
|
190
|
+
stopReason,
|
|
191
|
+
promptTokens,
|
|
192
|
+
outputTokens,
|
|
193
|
+
cachedPromptTokens: cachedTokens,
|
|
194
|
+
promptCacheUsage,
|
|
195
|
+
});
|
|
196
|
+
const indexFor = (outputIndex) => {
|
|
197
|
+
let index = toolIndex.get(outputIndex);
|
|
198
|
+
if (index === undefined) {
|
|
199
|
+
index = toolIndex.size;
|
|
200
|
+
toolIndex.set(outputIndex, index);
|
|
201
|
+
}
|
|
202
|
+
return index;
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
buildFallbackDone: done,
|
|
206
|
+
handleData(dataStr) {
|
|
207
|
+
if (dataStr === '[DONE]')
|
|
208
|
+
return [done()];
|
|
209
|
+
let data;
|
|
210
|
+
try {
|
|
211
|
+
data = JSON.parse(dataStr);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
switch (data.type) {
|
|
217
|
+
case 'response.output_text.delta':
|
|
218
|
+
return typeof data.delta === 'string' && data.delta
|
|
219
|
+
? [{ type: 'text_delta', delta: data.delta }]
|
|
220
|
+
: [];
|
|
221
|
+
case 'response.reasoning_summary_text.delta':
|
|
222
|
+
case 'response.reasoning_text.delta':
|
|
223
|
+
return typeof data.delta === 'string' && data.delta
|
|
224
|
+
? [{ type: 'reasoning_delta', field: 'reasoning', delta: data.delta }]
|
|
225
|
+
: [];
|
|
226
|
+
case 'response.output_item.added': {
|
|
227
|
+
const item = data.item;
|
|
228
|
+
if (item?.type !== 'function_call')
|
|
229
|
+
return [];
|
|
230
|
+
sawToolCall = true;
|
|
231
|
+
// id 用 call_id(回传 function_call_output 时必须对得上),不是 fc_ 的 item id。
|
|
232
|
+
return [{ type: 'tool_call_delta', index: indexFor(data.output_index ?? 0), id: item.call_id, name: item.name }];
|
|
233
|
+
}
|
|
234
|
+
case 'response.function_call_arguments.delta': {
|
|
235
|
+
const outputIndex = data.output_index ?? 0;
|
|
236
|
+
if (typeof data.delta !== 'string' || !data.delta)
|
|
237
|
+
return [];
|
|
238
|
+
streamedArguments.add(outputIndex);
|
|
239
|
+
return [{ type: 'tool_call_delta', index: indexFor(outputIndex), argumentsDelta: data.delta }];
|
|
240
|
+
}
|
|
241
|
+
case 'response.output_item.done': {
|
|
242
|
+
// 兜底:有的网关不发 arguments.delta,只在条目完成时给完整 arguments。
|
|
243
|
+
const item = data.item;
|
|
244
|
+
const outputIndex = data.output_index ?? 0;
|
|
245
|
+
if (item?.type !== 'function_call' || streamedArguments.has(outputIndex))
|
|
246
|
+
return [];
|
|
247
|
+
sawToolCall = true;
|
|
248
|
+
const arguments_ = typeof item.arguments === 'string' ? item.arguments : '';
|
|
249
|
+
return arguments_
|
|
250
|
+
? [{ type: 'tool_call_delta', index: indexFor(outputIndex), id: item.call_id, name: item.name, argumentsDelta: arguments_ }]
|
|
251
|
+
: [];
|
|
252
|
+
}
|
|
253
|
+
case 'response.completed':
|
|
254
|
+
readUsage(data.response?.usage);
|
|
255
|
+
stopReason = sawToolCall ? 'tool_calls' : 'stop';
|
|
256
|
+
return [done()];
|
|
257
|
+
case 'response.incomplete': {
|
|
258
|
+
readUsage(data.response?.usage);
|
|
259
|
+
const reason = data.response?.incomplete_details?.reason;
|
|
260
|
+
stopReason = reason === 'max_output_tokens' ? 'length' : sawToolCall ? 'tool_calls' : 'unknown';
|
|
261
|
+
return [done()];
|
|
262
|
+
}
|
|
263
|
+
case 'response.failed': {
|
|
264
|
+
const error = data.response?.error;
|
|
265
|
+
throw new Error(`OpenAI Responses 流式响应失败:${JSON.stringify(error ?? data.response ?? data)}`);
|
|
266
|
+
}
|
|
267
|
+
case 'error':
|
|
268
|
+
throw new Error(`OpenAI Responses 流式响应报错:${JSON.stringify(data.error ?? data)}`);
|
|
269
|
+
default:
|
|
270
|
+
// response.created / in_progress / content_part.* / output_text.done 等:已知安全的 no-op。
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|