@bitkyc08/opencodex 2.7.41 → 2.7.42
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/README.md +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
- package/gui/dist/assets/index-DfVGuN88.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/kiro-constants.ts +6 -2
- package/src/adapters/kiro-retry.ts +175 -10
- package/src/adapters/kiro.ts +172 -85
- package/src/adapters/mimo-free.ts +1 -0
- package/src/adapters/openai-chat.ts +30 -4
- package/src/adapters/openai-responses.ts +90 -12
- package/src/bridge.ts +91 -43
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +29 -2
- package/src/cli/access.ts +108 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account.ts +9 -1
- package/src/cli/agent.ts +184 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +20 -8
- package/src/cli/doctor.ts +45 -8
- package/src/cli/help.ts +65 -13
- package/src/cli/index.ts +108 -7
- package/src/cli/integrations.ts +142 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +9 -10
- package/src/cli/observe.ts +117 -0
- package/src/cli/provider-runtime.ts +152 -0
- package/src/cli/provider.ts +23 -1
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +3 -3
- package/src/cli/status.ts +17 -0
- package/src/cli/system-command.ts +112 -0
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/aggregation.ts +113 -18
- package/src/codex/catalog/provider-fetch.ts +24 -13
- package/src/codex/catalog/sync.ts +20 -8
- package/src/codex/catalog.ts +2 -1
- package/src/codex/refresh.ts +10 -3
- package/src/codex/routing.ts +21 -32
- package/src/codex/sync.ts +17 -0
- package/src/config.ts +48 -0
- package/src/generated/jawcode-model-metadata.ts +2 -1
- package/src/grok/inject.ts +184 -4
- package/src/grok/status.ts +33 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/windows-elevation.ts +627 -0
- package/src/providers/openai-sidecar.ts +46 -2
- package/src/providers/registry.ts +52 -0
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/claude-messages.ts +20 -1
- package/src/server/images.ts +14 -7
- package/src/server/management/agent-settings-routes.ts +10 -4
- package/src/server/management/combo-routes.ts +0 -1
- package/src/server/management/config-routes.ts +0 -1
- package/src/server/management/logs-usage-routes.ts +94 -0
- package/src/server/management/model-routes.ts +0 -1
- package/src/server/management/oauth-account-routes.ts +0 -1
- package/src/server/management/provider-routes.ts +0 -1
- package/src/server/management/shared.ts +0 -1
- package/src/server/management/system-routes.ts +27 -15
- package/src/server/management-api.ts +0 -1
- package/src/server/memory-watchdog.ts +54 -10
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +122 -2
- package/src/server/responses/core.ts +76 -13
- package/src/server/responses/passthrough-error.ts +38 -13
- package/src/server/startup-action-control.ts +266 -15
- package/src/service.ts +512 -3
- package/src/storage/cleanup.ts +1538 -0
- package/src/storage/scanner.ts +4 -1
- package/src/types.ts +16 -0
- package/src/update/job.ts +229 -25
- package/src/usage/log.ts +39 -0
- package/src/web-search/loop.ts +8 -1
- package/gui/dist/assets/index-B2J4t3te.css +0 -1
- package/gui/dist/assets/index-BmvM6wRb.js +0 -65
package/src/adapters/kiro.ts
CHANGED
|
@@ -33,7 +33,7 @@ import type { ProviderAdapter } from "./base";
|
|
|
33
33
|
import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
34
34
|
import { extractKiroImages, normalizeKiroImages, type KiroImage } from "./kiro-images";
|
|
35
35
|
import { sniffImageDimensions } from "./anthropic-image-guard";
|
|
36
|
-
import { fetchKiroWithRetry } from "./kiro-retry";
|
|
36
|
+
import { fetchKiroWithRetry, noteKiroTransientThrottle } from "./kiro-retry";
|
|
37
37
|
import { convertKiroToolContext } from "./kiro-tools";
|
|
38
38
|
import { neutralizeIdentity } from "./identity";
|
|
39
39
|
import { buildNonOpenAIToolCatalogNudgeFromNames } from "./tool-catalog-nudge";
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
KIRO_COMPLETION_RETRY_MESSAGE,
|
|
43
43
|
KIRO_COMPLETION_TOOL_NAME,
|
|
44
44
|
KIRO_CONTINUATION_MESSAGE,
|
|
45
|
+
KIRO_EMPTY_TOOL_RESULT_MESSAGE,
|
|
46
|
+
KIRO_TOOL_RESULT_CARRIER_MESSAGE,
|
|
45
47
|
MAX_KIRO_INJECTED_INSTRUCTION_CHARS,
|
|
46
48
|
type KiroCompletionMode,
|
|
47
49
|
} from "./kiro-constants";
|
|
@@ -305,6 +307,51 @@ function appendTurnText(target: string, next: string): string {
|
|
|
305
307
|
return target ? `${target}\n\n${next}` : next;
|
|
306
308
|
}
|
|
307
309
|
|
|
310
|
+
function validateKiroConversationState(history: KiroHistoryEntry[], currentMessage: KiroHistoryEntry): void {
|
|
311
|
+
const entries = [...history, currentMessage];
|
|
312
|
+
const pendingToolUses = new Set<string>();
|
|
313
|
+
let previousRole: "user" | "assistant" | undefined;
|
|
314
|
+
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
const user = entry.userInputMessage;
|
|
317
|
+
const assistant = entry.assistantResponseMessage;
|
|
318
|
+
if (Boolean(user) === Boolean(assistant)) {
|
|
319
|
+
throw new Error("Kiro conversation entries must contain exactly one message role");
|
|
320
|
+
}
|
|
321
|
+
const role = user ? "user" : "assistant";
|
|
322
|
+
if (role === previousRole) throw new Error("Kiro conversation roles must alternate");
|
|
323
|
+
previousRole = role;
|
|
324
|
+
|
|
325
|
+
if (user) {
|
|
326
|
+
const hasPayload = Boolean(user.content.trim())
|
|
327
|
+
|| Boolean(user.images?.length)
|
|
328
|
+
|| Boolean(user.userInputMessageContext?.toolResults?.length);
|
|
329
|
+
if (!hasPayload) throw new Error("Kiro user messages must not be empty");
|
|
330
|
+
for (const result of user.userInputMessageContext?.toolResults ?? []) {
|
|
331
|
+
if (!pendingToolUses.delete(result.toolUseId)) {
|
|
332
|
+
throw new Error(`Kiro tool result has no matching tool use ${JSON.stringify(result.toolUseId)}`);
|
|
333
|
+
}
|
|
334
|
+
if (!result.content.some(part => part.text.trim())) {
|
|
335
|
+
throw new Error(`Kiro tool result must not be empty ${JSON.stringify(result.toolUseId)}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const toolUses = assistant?.toolUses ?? [];
|
|
342
|
+
if (!assistant?.content.trim() && toolUses.length === 0) {
|
|
343
|
+
throw new Error("Kiro assistant messages must not be empty");
|
|
344
|
+
}
|
|
345
|
+
for (const toolUse of toolUses) {
|
|
346
|
+
if (pendingToolUses.has(toolUse.toolUseId)) {
|
|
347
|
+
throw new Error(`Kiro conversation contains duplicate tool use ${JSON.stringify(toolUse.toolUseId)}`);
|
|
348
|
+
}
|
|
349
|
+
pendingToolUses.add(toolUse.toolUseId);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (pendingToolUses.size > 0) throw new Error("Kiro conversation contains an unanswered tool use");
|
|
353
|
+
}
|
|
354
|
+
|
|
308
355
|
function boundedInjectedInstruction(text: string, used: { value: number }): string | undefined {
|
|
309
356
|
const remaining = MAX_KIRO_INJECTED_INSTRUCTION_CHARS - used.value;
|
|
310
357
|
if (remaining <= 0 || !text) return undefined;
|
|
@@ -423,31 +470,30 @@ export function buildKiroPayload(
|
|
|
423
470
|
throw new Error(`Kiro cannot translate encrypted output for tool call ${JSON.stringify(tr.toolCallId)}`);
|
|
424
471
|
}
|
|
425
472
|
const text = userContentText(tr.content);
|
|
473
|
+
const resultText = text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE;
|
|
426
474
|
const images = extractKiroImages(tr.content);
|
|
427
475
|
const toolUseId = normalizeToolId(tr.toolCallId);
|
|
428
476
|
if (!priorCalls.has(toolUseId)) {
|
|
429
477
|
throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`);
|
|
430
478
|
}
|
|
431
|
-
pushUser(
|
|
432
|
-
content: [{ text }],
|
|
479
|
+
pushUser(KIRO_TOOL_RESULT_CARRIER_MESSAGE, images, [{
|
|
480
|
+
content: [{ text: resultText }],
|
|
433
481
|
status: tr.isError ? "error" : "success",
|
|
434
482
|
toolUseId,
|
|
435
483
|
}]);
|
|
436
484
|
}
|
|
437
485
|
}
|
|
438
486
|
|
|
439
|
-
// A reasoning-only first attempt has no Kiro-replayable assistant text. Preserve the turn
|
|
440
|
-
// boundary structurally so the adapter-generated retry is still a user turn after assistant
|
|
441
|
-
// history, without inventing prose that the model never said.
|
|
442
|
-
if (completionMode === "text_fallback" && turns.at(-1)?.kind !== "assistant") {
|
|
443
|
-
pushAssistant("", []);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
487
|
if (turns.length === 0 || turns[0].kind === "assistant") {
|
|
447
488
|
turns.unshift({ kind: "user", content: KIRO_CONTINUATION_MESSAGE, images: [], toolResults: [] });
|
|
448
489
|
}
|
|
449
490
|
if (turns.at(-1)?.kind === "assistant") {
|
|
450
|
-
turns.push({
|
|
491
|
+
turns.push({
|
|
492
|
+
kind: "user",
|
|
493
|
+
content: completionMode === "text_fallback" ? KIRO_COMPLETION_RETRY_MESSAGE : KIRO_CONTINUATION_MESSAGE,
|
|
494
|
+
images: [],
|
|
495
|
+
toolResults: [],
|
|
496
|
+
});
|
|
451
497
|
}
|
|
452
498
|
|
|
453
499
|
const currentTurn = turns.pop();
|
|
@@ -481,11 +527,14 @@ export function buildKiroPayload(
|
|
|
481
527
|
currentUim.userInputMessageContext = { ...(currentUim.userInputMessageContext ?? {}), tools: kiroTools };
|
|
482
528
|
}
|
|
483
529
|
if (completionMode === "text_fallback") {
|
|
484
|
-
currentUim.content
|
|
530
|
+
if (currentUim.content !== KIRO_COMPLETION_RETRY_MESSAGE) {
|
|
531
|
+
currentUim.content = appendTurnText(currentUim.content, KIRO_COMPLETION_RETRY_MESSAGE);
|
|
532
|
+
}
|
|
485
533
|
} else if (!currentUim.userInputMessageContext?.toolResults && currentUim.content !== KIRO_CONTINUATION_MESSAGE) {
|
|
486
534
|
currentUim.content = injectKiroThinkingTags(currentUim.content, parsed);
|
|
487
535
|
}
|
|
488
536
|
|
|
537
|
+
validateKiroConversationState(history, currentEntry);
|
|
489
538
|
const conversationId = stableConversationId(parsed);
|
|
490
539
|
const payload: Record<string, unknown> = {
|
|
491
540
|
conversationState: {
|
|
@@ -586,37 +635,35 @@ function retryableKiroIncomplete(
|
|
|
586
635
|
message: string,
|
|
587
636
|
usage: OcxUsage,
|
|
588
637
|
providerState: { kiro: { conversationId: string } } | undefined,
|
|
638
|
+
retryable = true,
|
|
589
639
|
): AdapterEvent {
|
|
590
640
|
return {
|
|
591
641
|
type: "incomplete",
|
|
592
642
|
reason,
|
|
593
643
|
message,
|
|
594
644
|
usage,
|
|
595
|
-
retryable
|
|
645
|
+
retryable,
|
|
596
646
|
endTurn: false,
|
|
597
647
|
...(providerState ? { providerState } : {}),
|
|
598
648
|
};
|
|
599
649
|
}
|
|
600
650
|
|
|
601
651
|
/**
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
* fuzzy suppression can silently delete the one sentence that carries the result. Showing a
|
|
606
|
-
* cosmetic restatement is the cheaper failure.
|
|
652
|
+
* Catch-path retryability for #519: only transport/socket failures with no emitted output
|
|
653
|
+
* are replay-safe. Malformed event payloads (`invalid Kiro …`) and any post-output failure
|
|
654
|
+
* stay terminal — same spirit as cursor's emittedOutput gate.
|
|
607
655
|
*/
|
|
608
|
-
function
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
656
|
+
export function isRetryableKiroStreamCatchError(err: unknown, emittedOutput: boolean): boolean {
|
|
657
|
+
if (emittedOutput) return false;
|
|
658
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
659
|
+
if (/^invalid Kiro\b/i.test(message)) return false;
|
|
660
|
+
// Include Smithy/eventstream truncation (`eventstream: truncated message at end of stream`):
|
|
661
|
+
// partial frame + clean EOF with zero output is the same replay-safe class as a socket close.
|
|
662
|
+
return /socket connection was closed|connection(?: was)? closed unexpectedly|ECONNRESET|EPIPE|UND_ERR_|fetch failed|decoder failed|premature close|other side closed|unexpected EOF|network connection lost|terminated|truncated message at end of stream|eventstream:\s*truncated/i
|
|
663
|
+
.test(message);
|
|
614
664
|
}
|
|
615
665
|
|
|
616
|
-
/**
|
|
617
|
-
* Kiro's native stop reason for a turn the model considers finished. Only this value is
|
|
618
|
-
* authoritative; `TOOL_USE` and an absent reason both leave the turn incomplete.
|
|
619
|
-
*/
|
|
666
|
+
/** Native clean-stop reason eligible for bounded private-completion validation. */
|
|
620
667
|
const KIRO_END_TURN_STOP_REASON = "END_TURN";
|
|
621
668
|
|
|
622
669
|
async function* parseKiroAttempt(
|
|
@@ -627,13 +674,12 @@ async function* parseKiroAttempt(
|
|
|
627
674
|
contextWindowState: KiroContextWindowState,
|
|
628
675
|
nameMap: Map<string, string> | undefined,
|
|
629
676
|
conversationId: string | undefined,
|
|
630
|
-
previousAssistantText?: string,
|
|
631
677
|
contextInputEstimate?: number,
|
|
678
|
+
/** True when an earlier attempt already flushed visible content to the client (#520). */
|
|
679
|
+
priorEmittedOutput = false,
|
|
632
680
|
): AsyncGenerator<AdapterEvent, KiroAttemptResult> {
|
|
633
|
-
// `required` mode holds staged commentary
|
|
634
|
-
//
|
|
635
|
-
// — every early error return — is flushed before the terminal event so partial output is never
|
|
636
|
-
// silently dropped.
|
|
681
|
+
// `required` mode holds staged commentary until a real tool call or terminal metadata identifies
|
|
682
|
+
// the attempt boundary. Anything the inner parser leaves behind is flushed before the terminal.
|
|
637
683
|
const deferred: AdapterEvent[] = [];
|
|
638
684
|
const attempt = parseKiroAttemptEvents(
|
|
639
685
|
response,
|
|
@@ -644,8 +690,8 @@ async function* parseKiroAttempt(
|
|
|
644
690
|
nameMap,
|
|
645
691
|
conversationId,
|
|
646
692
|
deferred,
|
|
647
|
-
previousAssistantText,
|
|
648
693
|
contextInputEstimate,
|
|
694
|
+
priorEmittedOutput,
|
|
649
695
|
);
|
|
650
696
|
let next = await attempt.next();
|
|
651
697
|
while (!next.done) {
|
|
@@ -665,8 +711,8 @@ async function* parseKiroAttemptEvents(
|
|
|
665
711
|
nameMap: Map<string, string> | undefined,
|
|
666
712
|
conversationId: string | undefined,
|
|
667
713
|
deferred: AdapterEvent[],
|
|
668
|
-
previousAssistantText?: string,
|
|
669
714
|
contextInputEstimate?: number,
|
|
715
|
+
priorEmittedOutput = false,
|
|
670
716
|
): AsyncGenerator<AdapterEvent, KiroAttemptResult> {
|
|
671
717
|
const emptyResult = (): KiroAttemptResult => ({ assistantText: "", sawReasoning: false });
|
|
672
718
|
if (!response.body) {
|
|
@@ -717,15 +763,30 @@ async function* parseKiroAttemptEvents(
|
|
|
717
763
|
return contextTotal > 0 ? { ...base, contextTotalTokens: contextTotal } : base;
|
|
718
764
|
};
|
|
719
765
|
|
|
720
|
-
const classifiedTerminal = (failure: KiroErrorClassification): AdapterEvent =>
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
766
|
+
const classifiedTerminal = (failure: KiroErrorClassification): AdapterEvent => {
|
|
767
|
+
// Upstream exception/error frames can arrive after commentary was already staged (and will be
|
|
768
|
+
// flushed before this terminal is yielded). Replaying after that content would duplicate it.
|
|
769
|
+
const emittedOutput = priorEmittedOutput
|
|
770
|
+
|| sawText
|
|
771
|
+
|| sawReasoning
|
|
772
|
+
|| sawRealTool
|
|
773
|
+
|| assistantText.length > 0
|
|
774
|
+
|| deferred.length > 0
|
|
775
|
+
|| completionAnswer !== undefined
|
|
776
|
+
|| completionCalls > 0
|
|
777
|
+
|| open !== null
|
|
778
|
+
|| fallbackEvents.length > 0;
|
|
779
|
+
if (failure.status === 429 && failure.retryable) noteKiroTransientThrottle();
|
|
780
|
+
return {
|
|
781
|
+
type: "error",
|
|
782
|
+
message: failure.message,
|
|
783
|
+
status: failure.status,
|
|
784
|
+
errorType: failure.errorType,
|
|
785
|
+
code: failure.code,
|
|
786
|
+
retryable: emittedOutput ? false : failure.retryable,
|
|
787
|
+
usage: usage(),
|
|
788
|
+
};
|
|
789
|
+
};
|
|
729
790
|
|
|
730
791
|
const protocolTerminal = (message: string, malformedCompletion = false): AdapterEvent => {
|
|
731
792
|
if (mode === "text_fallback" && malformedCompletion) {
|
|
@@ -734,6 +795,8 @@ async function* parseKiroAttemptEvents(
|
|
|
734
795
|
message,
|
|
735
796
|
usage(),
|
|
736
797
|
providerState(),
|
|
798
|
+
// First-attempt progress was already flushed before this bounded fallback (#520).
|
|
799
|
+
!priorEmittedOutput,
|
|
737
800
|
);
|
|
738
801
|
}
|
|
739
802
|
return {
|
|
@@ -1012,13 +1075,11 @@ async function* parseKiroAttemptEvents(
|
|
|
1012
1075
|
...(contextWindowState.value ? { upstreamContextWindow: contextWindowState.value } : {}),
|
|
1013
1076
|
});
|
|
1014
1077
|
}
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
// here rather than further down, because the deferred text is relabeled to `final_answer`
|
|
1019
|
-
// in this branch — once it flushes as commentary below, that chance is gone.
|
|
1078
|
+
// Native stop metadata proves that this inference ended, but it does not prove that ordinary
|
|
1079
|
+
// text is a final answer. Kiro has emitted END_TURN for progress prose, so tool-enabled turns
|
|
1080
|
+
// still require the private completion call to distinguish commentary from completion (#531).
|
|
1020
1081
|
const normalizedStopReason = stopReason?.trim().toUpperCase();
|
|
1021
|
-
const
|
|
1082
|
+
const nativeCompletionStop = (normalizedStopReason === KIRO_END_TURN_STOP_REASON
|
|
1022
1083
|
|| normalizedStopReason === "STOP_SEQUENCE")
|
|
1023
1084
|
&& sawText
|
|
1024
1085
|
&& !sawRealTool
|
|
@@ -1031,31 +1092,19 @@ async function* parseKiroAttemptEvents(
|
|
|
1031
1092
|
sawReasoning,
|
|
1032
1093
|
sawRealTool,
|
|
1033
1094
|
completionCalls,
|
|
1034
|
-
|
|
1095
|
+
nativeCompletionStop,
|
|
1035
1096
|
...(stopReason !== undefined ? { stopReason } : {}),
|
|
1036
1097
|
assistantChars: assistantText.length,
|
|
1037
1098
|
});
|
|
1038
1099
|
|
|
1039
1100
|
if (mode === "required") {
|
|
1040
|
-
if (nativeEndTurn) {
|
|
1041
|
-
for (const event of deferred.splice(0)) {
|
|
1042
|
-
yield event.type === "text_delta" ? { ...event, phase: "final_answer" } : event;
|
|
1043
|
-
}
|
|
1044
|
-
return {
|
|
1045
|
-
assistantText,
|
|
1046
|
-
sawReasoning,
|
|
1047
|
-
terminal: { type: "done", usage: finalUsage, endTurn: true, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
1101
|
for (const event of deferred.splice(0)) yield event;
|
|
1051
1102
|
}
|
|
1052
1103
|
|
|
1053
1104
|
if (mode === "text_fallback") {
|
|
1054
1105
|
if (completionAnswer !== undefined) {
|
|
1055
1106
|
for (const event of fallbackEvents) yield event;
|
|
1056
|
-
|
|
1057
|
-
yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
|
|
1058
|
-
}
|
|
1107
|
+
yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
|
|
1059
1108
|
return {
|
|
1060
1109
|
assistantText,
|
|
1061
1110
|
sawReasoning,
|
|
@@ -1071,10 +1120,9 @@ async function* parseKiroAttemptEvents(
|
|
|
1071
1120
|
};
|
|
1072
1121
|
}
|
|
1073
1122
|
if (sawText) {
|
|
1074
|
-
const repeated = isRepeatedKiroAnswer(assistantText, previousAssistantText);
|
|
1075
1123
|
for (const event of fallbackEvents) {
|
|
1076
1124
|
if (event.type !== "text_delta") yield event;
|
|
1077
|
-
else
|
|
1125
|
+
else yield { ...event, phase: "final_answer" };
|
|
1078
1126
|
}
|
|
1079
1127
|
return {
|
|
1080
1128
|
assistantText,
|
|
@@ -1093,6 +1141,8 @@ async function* parseKiroAttemptEvents(
|
|
|
1093
1141
|
: "Kiro produced no final answer on its bounded completion retry",
|
|
1094
1142
|
finalUsage,
|
|
1095
1143
|
finalProviderState,
|
|
1144
|
+
// First-attempt progress was already flushed before this bounded fallback (#520).
|
|
1145
|
+
!priorEmittedOutput,
|
|
1096
1146
|
),
|
|
1097
1147
|
};
|
|
1098
1148
|
}
|
|
@@ -1112,13 +1162,23 @@ async function* parseKiroAttemptEvents(
|
|
|
1112
1162
|
terminal: { type: "done", usage: finalUsage, endTurn: false, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
|
|
1113
1163
|
};
|
|
1114
1164
|
}
|
|
1115
|
-
|
|
1165
|
+
if (mode === "required" && nativeCompletionStop) {
|
|
1166
|
+
return {
|
|
1167
|
+
assistantText,
|
|
1168
|
+
sawReasoning,
|
|
1169
|
+
needsFallback: true,
|
|
1170
|
+
usage: finalUsage,
|
|
1171
|
+
providerState: finalProviderState,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// An explicit non-completion stop reason has already terminated this inference. Converting it into
|
|
1116
1176
|
// another model request would hide truncation behind a second paid call, and for context
|
|
1117
1177
|
// exhaustion it would resubmit a request that cannot fit. Only a MISSING stop reason falls
|
|
1118
1178
|
// through to the bounded compatibility fallback below.
|
|
1119
1179
|
//
|
|
1120
|
-
// END_TURN and STOP_SEQUENCE with text
|
|
1121
|
-
//
|
|
1180
|
+
// END_TURN and STOP_SEQUENCE with text take the bounded validation path above; reaching here
|
|
1181
|
+
// with either means the turn produced no replayable text.
|
|
1122
1182
|
if (mode === "required" && normalizedStopReason !== undefined) {
|
|
1123
1183
|
const providerStateField = finalProviderState ? { providerState: finalProviderState } : {};
|
|
1124
1184
|
const incomplete = (reason: string, retryable: boolean) => ({
|
|
@@ -1195,6 +1255,22 @@ async function* parseKiroAttemptEvents(
|
|
|
1195
1255
|
},
|
|
1196
1256
|
};
|
|
1197
1257
|
} catch (err) {
|
|
1258
|
+
// Mid-stream socket closes after response.created / heartbeats only must stay retryable:
|
|
1259
|
+
// nothing was relayed to the client, so a string-body replay is safe (see #519 / cursor's
|
|
1260
|
+
// emittedOutput gate). Once any assistant text, reasoning, tool, or deferred content exists
|
|
1261
|
+
// — including content flushed by a prior attempt before a bounded fallback — fail closed;
|
|
1262
|
+
// the client may already have partial output. Protocol parse throws stay non-retryable even
|
|
1263
|
+
// with zero output.
|
|
1264
|
+
const emittedOutput = priorEmittedOutput
|
|
1265
|
+
|| sawText
|
|
1266
|
+
|| sawReasoning
|
|
1267
|
+
|| sawRealTool
|
|
1268
|
+
|| assistantText.length > 0
|
|
1269
|
+
|| deferred.length > 0
|
|
1270
|
+
|| completionAnswer !== undefined
|
|
1271
|
+
|| completionCalls > 0
|
|
1272
|
+
|| open !== null
|
|
1273
|
+
|| fallbackEvents.length > 0;
|
|
1198
1274
|
return {
|
|
1199
1275
|
assistantText,
|
|
1200
1276
|
sawReasoning,
|
|
@@ -1204,7 +1280,7 @@ async function* parseKiroAttemptEvents(
|
|
|
1204
1280
|
status: 502,
|
|
1205
1281
|
errorType: "server_error",
|
|
1206
1282
|
code: "kiro_stream_protocol_error",
|
|
1207
|
-
retryable:
|
|
1283
|
+
retryable: isRetryableKiroStreamCatchError(err, emittedOutput),
|
|
1208
1284
|
usage: usage(),
|
|
1209
1285
|
},
|
|
1210
1286
|
};
|
|
@@ -1231,7 +1307,6 @@ export async function* parseKiroStream(
|
|
|
1231
1307
|
contextWindowState,
|
|
1232
1308
|
nameMap,
|
|
1233
1309
|
conversationId,
|
|
1234
|
-
undefined,
|
|
1235
1310
|
contextInputEstimate,
|
|
1236
1311
|
);
|
|
1237
1312
|
let firstNext = await first.next();
|
|
@@ -1255,6 +1330,10 @@ export async function* parseKiroStream(
|
|
|
1255
1330
|
}
|
|
1256
1331
|
|
|
1257
1332
|
yield { type: "heartbeat" };
|
|
1333
|
+
// First attempt already flushed deferred progress before this point. Gate fallback
|
|
1334
|
+
// setup/HTTP failures the same way as the second-stream catch so a replay cannot
|
|
1335
|
+
// duplicate visible commentary (#520).
|
|
1336
|
+
const priorEmittedOutput = Boolean(firstResult.assistantText.trim()) || firstResult.sawReasoning;
|
|
1258
1337
|
let fallback: KiroFallbackAttempt;
|
|
1259
1338
|
try {
|
|
1260
1339
|
fallback = await fallbackFactory(
|
|
@@ -1268,7 +1347,7 @@ export async function* parseKiroStream(
|
|
|
1268
1347
|
message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)),
|
|
1269
1348
|
status: err instanceof Error && err.name === "TimeoutError" ? 504 : 502,
|
|
1270
1349
|
errorType: "upstream_error",
|
|
1271
|
-
retryable:
|
|
1350
|
+
retryable: !priorEmittedOutput,
|
|
1272
1351
|
usage: firstResult.usage,
|
|
1273
1352
|
};
|
|
1274
1353
|
return;
|
|
@@ -1282,7 +1361,7 @@ export async function* parseKiroStream(
|
|
|
1282
1361
|
status: failure.status,
|
|
1283
1362
|
errorType: failure.errorType,
|
|
1284
1363
|
code: failure.code,
|
|
1285
|
-
retryable: failure.retryable,
|
|
1364
|
+
retryable: priorEmittedOutput ? false : failure.retryable,
|
|
1286
1365
|
usage: firstResult.usage,
|
|
1287
1366
|
};
|
|
1288
1367
|
return;
|
|
@@ -1296,8 +1375,10 @@ export async function* parseKiroStream(
|
|
|
1296
1375
|
contextWindowState,
|
|
1297
1376
|
fallback.nameMap,
|
|
1298
1377
|
fallback.conversationId,
|
|
1299
|
-
firstResult.assistantText,
|
|
1300
1378
|
fallback.contextInputEstimate,
|
|
1379
|
+
// First attempt already flushed deferred progress to the client before this fallback.
|
|
1380
|
+
// A zero-output transport failure here must stay non-retryable to avoid duplicating that text.
|
|
1381
|
+
priorEmittedOutput,
|
|
1301
1382
|
);
|
|
1302
1383
|
let secondNext = await second.next();
|
|
1303
1384
|
while (!secondNext.done) {
|
|
@@ -1312,12 +1393,17 @@ export async function* parseKiroStream(
|
|
|
1312
1393
|
mergeKiroUsage(firstResult.usage, secondResult.usage, Boolean(firstResult.assistantText))
|
|
1313
1394
|
?? { inputTokens, outputTokens: 0, estimated: true },
|
|
1314
1395
|
secondResult.providerState ?? firstResult.providerState,
|
|
1396
|
+
!priorEmittedOutput,
|
|
1315
1397
|
);
|
|
1316
1398
|
return;
|
|
1317
1399
|
}
|
|
1318
1400
|
if (secondResult.terminal.type === "done" || secondResult.terminal.type === "incomplete") {
|
|
1319
1401
|
yield {
|
|
1320
1402
|
...secondResult.terminal,
|
|
1403
|
+
// Belt-and-suspenders: never advertise a replay-safe incomplete after flushed progress.
|
|
1404
|
+
...(secondResult.terminal.type === "incomplete" && priorEmittedOutput
|
|
1405
|
+
? { retryable: false as const }
|
|
1406
|
+
: {}),
|
|
1321
1407
|
usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, Boolean(firstResult.assistantText)),
|
|
1322
1408
|
providerState: secondResult.terminal.providerState ?? firstResult.providerState,
|
|
1323
1409
|
};
|
|
@@ -1407,7 +1493,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1407
1493
|
const fallbackFactory: KiroFallbackFactory = async (
|
|
1408
1494
|
returnedConversationId,
|
|
1409
1495
|
assistantText,
|
|
1410
|
-
|
|
1496
|
+
_sawReasoning,
|
|
1411
1497
|
) => {
|
|
1412
1498
|
if (!requestSnapshot) throw new Error("Kiro completion retry lost its request state");
|
|
1413
1499
|
if (requestAbortSignal?.aborted) {
|
|
@@ -1420,16 +1506,17 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1420
1506
|
...(retryParsed._providerContinuation ?? {}),
|
|
1421
1507
|
...(returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : {}),
|
|
1422
1508
|
};
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1509
|
+
// Reasoning is not replayable on the Kiro wire. Adding an empty assistant turn merely to mark
|
|
1510
|
+
// that reasoning existed creates REQUEST_BODY_INVALID; only visible text earns a replay turn.
|
|
1511
|
+
if (assistantText.trim()) {
|
|
1512
|
+
retryParsed.context.messages.push({
|
|
1513
|
+
role: "assistant",
|
|
1514
|
+
content: [{ type: "text" as const, text: assistantText }],
|
|
1515
|
+
phase: "commentary",
|
|
1516
|
+
model: retryParsed.modelId,
|
|
1517
|
+
timestamp: Date.now(),
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1433
1520
|
const retry = await build(retryParsed, "text_fallback");
|
|
1434
1521
|
const response = await fetchKiroWithRetry(retry.request, {
|
|
1435
1522
|
abortSignal: requestAbortSignal,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderAdapter } from "./base";
|
|
1
|
+
import type { AdapterRequest, ProviderAdapter } from "./base";
|
|
2
2
|
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
|
|
3
3
|
import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
|
|
4
4
|
import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
@@ -349,7 +349,9 @@ function ensureZenRootObjectSchema(schema: unknown): Record<string, unknown> {
|
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
|
|
352
|
-
|
|
352
|
+
const baseUrl = provider.baseUrl.replace(/\/+$/, "");
|
|
353
|
+
return baseUrl === "https://opencode.ai/zen/v1"
|
|
354
|
+
|| baseUrl === "https://opencode.ai/zen/go/v1";
|
|
353
355
|
}
|
|
354
356
|
|
|
355
357
|
const XAI_SCHEMA_BASE_URLS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
|
|
@@ -544,19 +546,37 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
544
546
|
}
|
|
545
547
|
if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
|
|
546
548
|
const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
549
|
+
let reasoningLog: AdapterRequest["reasoningLog"];
|
|
547
550
|
if (reasoningEffort !== undefined) {
|
|
548
551
|
if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
|
|
549
552
|
const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
|
|
550
|
-
if (budget !== undefined)
|
|
553
|
+
if (budget !== undefined) {
|
|
554
|
+
body.thinking_budget = budget;
|
|
555
|
+
reasoningLog = {
|
|
556
|
+
effectiveEffort: parsed.options.reasoning === "minimal" ? "minimal" : reasoningEffort,
|
|
557
|
+
wireField: "thinking_budget",
|
|
558
|
+
wireValue: budget,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
551
561
|
} else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
|
|
552
562
|
// Vendor thinking-toggle wire: the mapped value is sent as `thinking: {type}` because
|
|
553
563
|
// these models ignore/reject reasoning_effort. Most use enabled/disabled; MiniMax-M3
|
|
554
564
|
// uses adaptive/disabled.
|
|
555
565
|
if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") {
|
|
556
566
|
body.thinking = { type: reasoningEffort };
|
|
567
|
+
reasoningLog = {
|
|
568
|
+
effectiveEffort: reasoningEffort,
|
|
569
|
+
wireField: "thinking.type",
|
|
570
|
+
wireValue: reasoningEffort,
|
|
571
|
+
};
|
|
557
572
|
}
|
|
558
573
|
} else {
|
|
559
574
|
body.reasoning_effort = reasoningEffort;
|
|
575
|
+
reasoningLog = {
|
|
576
|
+
effectiveEffort: reasoningEffort,
|
|
577
|
+
wireField: "reasoning_effort",
|
|
578
|
+
wireValue: reasoningEffort,
|
|
579
|
+
};
|
|
560
580
|
}
|
|
561
581
|
}
|
|
562
582
|
if (parsed.options.presencePenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
|
|
@@ -610,7 +630,13 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
610
630
|
});
|
|
611
631
|
}
|
|
612
632
|
|
|
613
|
-
return {
|
|
633
|
+
return {
|
|
634
|
+
url,
|
|
635
|
+
method: "POST",
|
|
636
|
+
headers,
|
|
637
|
+
body: bodyJson,
|
|
638
|
+
...(reasoningLog ? { reasoningLog } : {}),
|
|
639
|
+
};
|
|
614
640
|
},
|
|
615
641
|
|
|
616
642
|
async *parseStream(response: Response): AsyncGenerator<AdapterEvent> {
|