@bitkyc08/opencodex 2.7.40 → 2.7.41

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.
Files changed (110) hide show
  1. package/README.md +63 -12
  2. package/assets/claude-code-models.gif +0 -0
  3. package/gui/dist/assets/index-B2J4t3te.css +1 -0
  4. package/gui/dist/assets/index-BmvM6wRb.js +65 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -2
  7. package/src/adapters/google.ts +48 -9
  8. package/src/adapters/kiro-events.ts +15 -3
  9. package/src/adapters/kiro.ts +292 -28
  10. package/src/adapters/openai-chat.ts +63 -15
  11. package/src/adapters/openai-responses.ts +18 -0
  12. package/src/bridge.ts +76 -21
  13. package/src/chat/outbound.ts +66 -34
  14. package/src/claude/auth-detect.ts +229 -0
  15. package/src/claude/auth-mode-migration.ts +32 -0
  16. package/src/claude/auth-mode.ts +62 -0
  17. package/src/claude/desktop-3p-guard.ts +35 -0
  18. package/src/claude/desktop-3p.ts +121 -21
  19. package/src/claude/desktop-health.ts +26 -0
  20. package/src/claude/desktop-profile.ts +263 -0
  21. package/src/claude/inbound-debug.ts +4 -0
  22. package/src/claude/model-info.ts +9 -3
  23. package/src/cli/account-extended.ts +34 -0
  24. package/src/cli/account.ts +3 -1
  25. package/src/cli/claude-desktop.ts +152 -0
  26. package/src/cli/claude.ts +37 -3
  27. package/src/cli/doctor.ts +187 -6
  28. package/src/cli/help.ts +13 -1
  29. package/src/cli/index.ts +151 -57
  30. package/src/cli/status-oauth.ts +68 -0
  31. package/src/cli/status.ts +4 -0
  32. package/src/codex/account-lifecycle.ts +31 -0
  33. package/src/codex/auth-api.ts +133 -26
  34. package/src/codex/auth-collision.ts +55 -11
  35. package/src/codex/auth-context.ts +50 -6
  36. package/src/codex/catalog/provider-fetch.ts +6 -2
  37. package/src/codex/home.ts +61 -1
  38. package/src/codex/inject.ts +28 -66
  39. package/src/codex/injected-marker.ts +72 -0
  40. package/src/codex/journal.ts +39 -3
  41. package/src/codex/main-account-cache.ts +25 -0
  42. package/src/codex/model-cache.ts +20 -1
  43. package/src/codex/paths.ts +5 -0
  44. package/src/codex/routing.ts +138 -11
  45. package/src/codex/subagent-model-fallback.ts +455 -0
  46. package/src/codex/sync.ts +17 -0
  47. package/src/combos/failover.ts +10 -2
  48. package/src/combos/index.ts +1 -0
  49. package/src/combos/types.ts +9 -0
  50. package/src/config.ts +117 -0
  51. package/src/grok/inject.ts +339 -0
  52. package/src/grok/status.ts +88 -0
  53. package/src/grok/sync.ts +66 -0
  54. package/src/lib/destination-policy.ts +13 -0
  55. package/src/lib/errors.ts +59 -0
  56. package/src/lib/privacy.ts +9 -0
  57. package/src/lib/process-control.ts +57 -4
  58. package/src/oauth/health.ts +375 -0
  59. package/src/oauth/index.ts +69 -22
  60. package/src/oauth/kiro.ts +5 -3
  61. package/src/oauth/log.ts +48 -0
  62. package/src/oauth/store.ts +55 -6
  63. package/src/providers/alibaba-region-backup.ts +75 -0
  64. package/src/providers/alibaba-region-migration.ts +143 -0
  65. package/src/providers/alibaba-region-startup.ts +36 -0
  66. package/src/providers/api-keys.ts +5 -5
  67. package/src/providers/derive.ts +22 -1
  68. package/src/providers/free-directory.ts +181 -0
  69. package/src/providers/key-failover.ts +2 -2
  70. package/src/providers/kiro-models.ts +3 -2
  71. package/src/providers/openai-tiers.ts +1 -1
  72. package/src/providers/provider-id-rewrite.ts +150 -0
  73. package/src/providers/registry.ts +49 -2
  74. package/src/responses/parser.ts +49 -20
  75. package/src/responses/state.ts +156 -2
  76. package/src/router.ts +73 -5
  77. package/src/server/chat-completions.ts +78 -22
  78. package/src/server/claude-messages.ts +8 -0
  79. package/src/server/images.ts +2 -1
  80. package/src/server/index.ts +50 -30
  81. package/src/server/live.ts +2 -1
  82. package/src/server/management/agent-settings-routes.ts +337 -18
  83. package/src/server/management/api-access.ts +141 -0
  84. package/src/server/management/combo-routes.ts +3 -3
  85. package/src/server/management/config-routes.ts +4 -4
  86. package/src/server/management/logs-usage-routes.ts +64 -4
  87. package/src/server/management/model-routes.ts +114 -8
  88. package/src/server/management/oauth-account-routes.ts +31 -5
  89. package/src/server/management/provider-routes.ts +42 -11
  90. package/src/server/management/shared.ts +81 -2
  91. package/src/server/management/system-routes.ts +6 -1
  92. package/src/server/management-api.ts +24 -7
  93. package/src/server/port-reclaim.ts +261 -0
  94. package/src/server/request-log.ts +11 -5
  95. package/src/server/responses/collaboration.ts +11 -6
  96. package/src/server/responses/compact.ts +2 -1
  97. package/src/server/responses/core.ts +358 -155
  98. package/src/server/responses/passthrough-error.ts +53 -0
  99. package/src/server/search.ts +2 -1
  100. package/src/server/system-env.ts +23 -6
  101. package/src/server/windows-tcp-drop.ts +174 -0
  102. package/src/service.ts +43 -3
  103. package/src/types.ts +74 -6
  104. package/src/update/index.ts +30 -19
  105. package/src/update/job.ts +30 -15
  106. package/src/usage/log.ts +158 -2
  107. package/src/usage/summary.ts +8 -4
  108. package/src/web-search/loop.ts +4 -1
  109. package/gui/dist/assets/index-CMip1DzF.css +0 -1
  110. package/gui/dist/assets/index-cydcmbzC.js +0 -52
@@ -32,6 +32,7 @@ import type {
32
32
  import type { ProviderAdapter } from "./base";
33
33
  import type { AdapterFetchContext, AdapterRequest } from "./base";
34
34
  import { extractKiroImages, normalizeKiroImages, type KiroImage } from "./kiro-images";
35
+ import { sniffImageDimensions } from "./anthropic-image-guard";
35
36
  import { fetchKiroWithRetry } from "./kiro-retry";
36
37
  import { convertKiroToolContext } from "./kiro-tools";
37
38
  import { neutralizeIdentity } from "./identity";
@@ -134,6 +135,52 @@ function messageLogText(msg: OcxMessage): string {
134
135
  }).filter(Boolean).join("\n");
135
136
  }
136
137
 
138
+ function estimateKiroImageTokens(image: KiroImage): number {
139
+ const dimensions = sniffImageDimensions(image.source.bytes);
140
+ if (dimensions) {
141
+ return Math.max(256, Math.ceil(dimensions.width * dimensions.height / 750));
142
+ }
143
+ const decodedBytes = Math.floor(image.source.bytes.length * 3 / 4);
144
+ return Math.max(256, Math.ceil(decodedBytes / 512));
145
+ }
146
+
147
+ function estimateKiroTokens(text: string, modelId?: string): number {
148
+ return estimateTokens(text, modelId ? `kiro/${modelId}` : "kiro");
149
+ }
150
+
151
+ function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelId: string): number {
152
+ const conversationState = (payload as {
153
+ conversationState?: {
154
+ history?: KiroHistoryEntry[];
155
+ currentMessage?: KiroHistoryEntry;
156
+ };
157
+ }).conversationState;
158
+ if (!conversationState) return 0;
159
+
160
+ const parts: string[] = [];
161
+ let imageTokens = 0;
162
+ const entries = [
163
+ ...(conversationState.history ?? []),
164
+ ...(conversationState.currentMessage ? [conversationState.currentMessage] : []),
165
+ ];
166
+ for (const entry of entries) {
167
+ const user = entry.userInputMessage;
168
+ if (user) {
169
+ if (user.content) parts.push(user.content);
170
+ for (const image of user.images ?? []) imageTokens += estimateKiroImageTokens(image);
171
+ const context = user.userInputMessageContext;
172
+ if (context?.tools?.length) parts.push(serializeForUsage(context.tools));
173
+ if (context?.toolResults?.length) parts.push(serializeForUsage(context.toolResults));
174
+ }
175
+ const assistant = entry.assistantResponseMessage;
176
+ if (assistant) {
177
+ if (assistant.content) parts.push(assistant.content);
178
+ if (assistant.toolUses?.length) parts.push(serializeForUsage(assistant.toolUses));
179
+ }
180
+ }
181
+ return estimateKiroTokens(parts.join("\n"), modelId) + imageTokens;
182
+ }
183
+
137
184
  function shouldCountStablePromptOverhead(parsed: OcxParsedRequest): boolean {
138
185
  return !parsed.previousResponseId && !parsed.context.messages.some(m => m.role === "assistant");
139
186
  }
@@ -148,25 +195,21 @@ function estimateKiroInputTokens(parsed: OcxParsedRequest): number {
148
195
  if (parsed.context.tools?.length) parts.push(serializeForUsage(parsed.context.tools));
149
196
  }
150
197
 
151
- return estimateTokens(parts.join("\n"), parsed.modelId);
198
+ return estimateKiroTokens(parts.join("\n"), parsed.modelId);
152
199
  }
153
200
 
154
201
  function estimateKiroLogInputTokens(parsed: OcxParsedRequest): number {
155
202
  const parts = parsed.context.messages.map(messageLogText).filter(Boolean);
156
203
  if (parsed.context.systemPrompt?.length) parts.push(...parsed.context.systemPrompt);
157
204
  if (parsed.context.tools?.length) parts.push(serializeForUsage(parsed.context.tools));
158
- return Math.max(estimateKiroInputTokens(parsed), estimateTokens(parts.join("\n"), parsed.modelId));
205
+ return Math.max(estimateKiroInputTokens(parsed), estimateKiroTokens(parts.join("\n"), parsed.modelId));
159
206
  }
160
207
 
161
- function configuredKiroContextWindow(provider: OcxProviderConfig, modelId: string | undefined): number | undefined {
208
+ function kiroUpstreamContextWindow(modelId: string | undefined): number | undefined {
162
209
  if (!modelId) return undefined;
163
210
  const normalizedModelId = normalizeKiroModelId(modelId);
164
211
  if (normalizedModelId === "auto") return undefined;
165
- const window =
166
- modelRecordValue(provider.modelContextWindows, modelId)
167
- ?? modelRecordValue(provider.modelContextWindows, normalizedModelId)
168
- ?? provider.contextWindow
169
- ?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
212
+ const window = modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
170
213
  ?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, normalizedModelId);
171
214
  return typeof window === "number" && Number.isFinite(window) && window > 0 ? window : undefined;
172
215
  }
@@ -184,8 +227,22 @@ function kiroRuntimeEndpoint(provider: OcxProviderConfig, region: string): strin
184
227
 
185
228
  export type KiroReasoningMode = "native" | "emulated";
186
229
 
230
+ // Kiro takes a verified native effort field for these models, and each model family names it
231
+ // differently: the Sol-only `reasoning.effort` versus the Claude-specific `output_config.effort`.
232
+ // Models absent from this table fall back to emulated thinking instructions.
233
+ const KIRO_NATIVE_EFFORT_FIELDS: Record<string, "reasoning" | "output_config"> = {
234
+ "gpt-5.6-sol": "reasoning",
235
+ "claude-opus-5": "output_config",
236
+ };
237
+
238
+ const KIRO_NATIVE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
239
+
240
+ function kiroNativeEffortField(modelId: string): "reasoning" | "output_config" | undefined {
241
+ return KIRO_NATIVE_EFFORT_FIELDS[normalizeKiroModelId(modelId)];
242
+ }
243
+
187
244
  export function kiroReasoningMode(modelId: string): KiroReasoningMode {
188
- return normalizeKiroModelId(modelId) === "gpt-5.6-sol" ? "native" : "emulated";
245
+ return kiroNativeEffortField(modelId) ? "native" : "emulated";
189
246
  }
190
247
 
191
248
  function kiroThinkingBudget(parsed: OcxParsedRequest): number | undefined {
@@ -439,11 +496,12 @@ export function buildKiroPayload(
439
496
  },
440
497
  };
441
498
  const effort = parsed.options.reasoning;
442
- if (kiroReasoningMode(parsed.modelId) === "native" && effort && effort !== "none") {
443
- if (!["low", "medium", "high", "xhigh", "max"].includes(effort)) {
444
- throw new Error(`Kiro gpt-5.6-sol does not support reasoning effort ${JSON.stringify(effort)}`);
499
+ const effortField = kiroNativeEffortField(parsed.modelId);
500
+ if (effortField && effort && effort !== "none") {
501
+ if (!KIRO_NATIVE_EFFORTS.includes(effort)) {
502
+ throw new Error(`Kiro ${normalizeKiroModelId(parsed.modelId)} does not support reasoning effort ${JSON.stringify(effort)}`);
445
503
  }
446
- payload.additionalModelRequestFields = { reasoning: { effort } };
504
+ payload.additionalModelRequestFields = { [effortField]: { effort } };
447
505
  }
448
506
  if (profileArn) payload.profileArn = profileArn;
449
507
  return { payload, nameMap, conversationId, completionMode };
@@ -465,17 +523,26 @@ interface KiroAttemptResult {
465
523
  interface KiroFallbackAttempt {
466
524
  response: Response;
467
525
  inputTokens: number;
526
+ contextInputEstimate: number;
468
527
  nameMap: Map<string, string>;
469
528
  conversationId: string;
470
529
  }
471
530
 
531
+ interface KiroContextWindowState {
532
+ value?: number;
533
+ }
534
+
472
535
  type KiroFallbackFactory = (
473
536
  conversationId: string | undefined,
474
537
  assistantText: string,
475
538
  sawReasoning: boolean,
476
539
  ) => Promise<KiroFallbackAttempt>;
477
540
 
478
- function mergeKiroUsage(first: OcxUsage | undefined, second: OcxUsage | undefined): OcxUsage | undefined {
541
+ function mergeKiroUsage(
542
+ first: OcxUsage | undefined,
543
+ second: OcxUsage | undefined,
544
+ preserveFirstContextGrowth = false,
545
+ ): OcxUsage | undefined {
479
546
  if (!first) return second;
480
547
  if (!second) return first;
481
548
  const sumOptional = (key: keyof OcxUsage): number | undefined => {
@@ -488,9 +555,23 @@ function mergeKiroUsage(first: OcxUsage | undefined, second: OcxUsage | undefine
488
555
  const totalTokens = typeof first.totalTokens === "number" && typeof second.totalTokens === "number"
489
556
  ? first.totalTokens + second.totalTokens
490
557
  : undefined;
558
+ const carriedContextTotal = preserveFirstContextGrowth && typeof first.contextTotalTokens === "number"
559
+ ? first.contextTotalTokens + second.outputTokens
560
+ : undefined;
561
+ const combinedOutputTokens = first.outputTokens + second.outputTokens;
491
562
  return {
492
563
  inputTokens: first.inputTokens + second.inputTokens,
493
- outputTokens: first.outputTokens + second.outputTokens,
564
+ outputTokens: combinedOutputTokens,
565
+ ...(typeof first.contextTotalTokens === "number" || typeof second.contextTotalTokens === "number"
566
+ ? {
567
+ contextTotalTokens: Math.max(
568
+ first.contextTotalTokens ?? 0,
569
+ second.contextTotalTokens ?? 0,
570
+ carriedContextTotal ?? 0,
571
+ combinedOutputTokens,
572
+ ),
573
+ }
574
+ : {}),
494
575
  ...(totalTokens !== undefined ? { totalTokens } : {}),
495
576
  ...(sumOptional("cachedInputTokens") !== undefined ? { cachedInputTokens: sumOptional("cachedInputTokens") } : {}),
496
577
  ...(sumOptional("cacheReadInputTokens") !== undefined ? { cacheReadInputTokens: sumOptional("cacheReadInputTokens") } : {}),
@@ -517,6 +598,13 @@ function retryableKiroIncomplete(
517
598
  };
518
599
  }
519
600
 
601
+ /**
602
+ * Suppress only a whitespace-normalized exact repeat. Semantic/fuzzy matching was rejected
603
+ * during review: two long near-identical messages can differ by a single status word
604
+ * ("is still pending" -> "is now complete") and still clear every similarity threshold, so
605
+ * fuzzy suppression can silently delete the one sentence that carries the result. Showing a
606
+ * cosmetic restatement is the cheaper failure.
607
+ */
520
608
  function normalizedKiroAnswer(text: string): string {
521
609
  return text.trim().replace(/\s+/g, " ");
522
610
  }
@@ -525,15 +613,60 @@ function isRepeatedKiroAnswer(text: string, previous?: string): boolean {
525
613
  return normalizedKiroAnswer(text) === normalizedKiroAnswer(previous ?? "");
526
614
  }
527
615
 
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
+ */
620
+ const KIRO_END_TURN_STOP_REASON = "END_TURN";
621
+
528
622
  async function* parseKiroAttempt(
529
623
  response: Response,
530
624
  mode: KiroCompletionMode,
531
625
  modelId: string | undefined,
532
626
  inputTokens: number,
533
- contextWindow: number | undefined,
627
+ contextWindowState: KiroContextWindowState,
534
628
  nameMap: Map<string, string> | undefined,
535
629
  conversationId: string | undefined,
536
630
  previousAssistantText?: string,
631
+ contextInputEstimate?: number,
632
+ ): AsyncGenerator<AdapterEvent, KiroAttemptResult> {
633
+ // `required` mode holds staged commentary here so a terminal END_TURN can relabel it as the final
634
+ // answer instead of paying for another inference request. Anything the inner parser leaves behind
635
+ // — every early error return — is flushed before the terminal event so partial output is never
636
+ // silently dropped.
637
+ const deferred: AdapterEvent[] = [];
638
+ const attempt = parseKiroAttemptEvents(
639
+ response,
640
+ mode,
641
+ modelId,
642
+ inputTokens,
643
+ contextWindowState,
644
+ nameMap,
645
+ conversationId,
646
+ deferred,
647
+ previousAssistantText,
648
+ contextInputEstimate,
649
+ );
650
+ let next = await attempt.next();
651
+ while (!next.done) {
652
+ yield next.value;
653
+ next = await attempt.next();
654
+ }
655
+ for (const event of deferred.splice(0)) yield event;
656
+ return next.value;
657
+ }
658
+
659
+ async function* parseKiroAttemptEvents(
660
+ response: Response,
661
+ mode: KiroCompletionMode,
662
+ modelId: string | undefined,
663
+ inputTokens: number,
664
+ contextWindowState: KiroContextWindowState,
665
+ nameMap: Map<string, string> | undefined,
666
+ conversationId: string | undefined,
667
+ deferred: AdapterEvent[],
668
+ previousAssistantText?: string,
669
+ contextInputEstimate?: number,
537
670
  ): AsyncGenerator<AdapterEvent, KiroAttemptResult> {
538
671
  const emptyResult = (): KiroAttemptResult => ({ assistantText: "", sawReasoning: false });
539
672
  if (!response.body) {
@@ -554,17 +687,35 @@ async function* parseKiroAttempt(
554
687
  let completionAnswer: string | undefined;
555
688
  let completionCalls = 0;
556
689
  let authoritativeUsage: OcxUsage | undefined;
690
+ let stopReason: string | undefined;
557
691
  const fallbackEvents: AdapterEvent[] = [];
558
692
  const thinking = new KiroThinkingParser();
559
693
 
560
694
  const providerState = (): { kiro: { conversationId: string } } | undefined =>
561
695
  returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : undefined;
562
696
 
563
- const usage = (): OcxUsage => authoritativeUsage ?? ({
697
+ const contextUsageTotalFloor = (): number | undefined => {
698
+ if (contextUsagePercentage === undefined || !contextWindowState.value) return undefined;
699
+ const floor = Math.ceil(contextWindowState.value * Math.min(contextUsagePercentage, 100) / 100);
700
+ return Number.isFinite(floor) && floor > 0 ? floor : undefined;
701
+ };
702
+ const usage = (): OcxUsage => {
703
+ const base = authoritativeUsage ?? {
564
704
  inputTokens,
565
- outputTokens: estimateTokens(outputChars, modelId),
705
+ outputTokens: estimateKiroTokens(outputChars, modelId),
566
706
  estimated: true,
567
- });
707
+ };
708
+ const estimatedContextTotal = contextInputEstimate !== undefined
709
+ ? contextInputEstimate + base.outputTokens
710
+ : undefined;
711
+ const authoritativeTurnTotal = base.inputTokens + base.outputTokens;
712
+ const contextTotal = Math.max(
713
+ estimatedContextTotal ?? 0,
714
+ contextUsageTotalFloor() ?? 0,
715
+ authoritativeTurnTotal,
716
+ );
717
+ return contextTotal > 0 ? { ...base, contextTotalTokens: contextTotal } : base;
718
+ };
568
719
 
569
720
  const classifiedTerminal = (failure: KiroErrorClassification): AdapterEvent => ({
570
721
  type: "error",
@@ -627,6 +778,17 @@ async function* parseKiroAttempt(
627
778
  return terminal ? { terminal } : { tool: next };
628
779
  };
629
780
 
781
+ // In `required` mode Kiro's stop reason only arrives on the terminal metadata event, so staged
782
+ // commentary is held until either a real tool call proves the turn continues (flush as
783
+ // commentary) or the stream ends (relabel as the final answer when END_TURN says so). A heartbeat
784
+ // stands in for each held event so the bridge's stall watchdog stays armed.
785
+ const defer = (event: AdapterEvent): AdapterEvent[] => {
786
+ if (sawRealTool) return [...deferred.splice(0), event];
787
+ if (event.type !== "text_delta" && deferred.length === 0) return [event];
788
+ deferred.push(event);
789
+ return [{ type: "heartbeat" }];
790
+ };
791
+
630
792
  const stage = (event: AdapterEvent): AdapterEvent[] => {
631
793
  if (event.type === "text_delta") {
632
794
  assistantText += event.text;
@@ -639,7 +801,7 @@ async function* parseKiroAttempt(
639
801
  fallbackEvents.push(phased);
640
802
  return [];
641
803
  }
642
- return [phased];
804
+ return mode === "required" ? defer(phased) : [phased];
643
805
  }
644
806
  if (event.type === "reasoning_raw_delta" || event.type === "thinking_delta") {
645
807
  const text = event.type === "reasoning_raw_delta" ? event.text : event.thinking;
@@ -650,7 +812,7 @@ async function* parseKiroAttempt(
650
812
  fallbackEvents.push(event);
651
813
  return [];
652
814
  }
653
- return [event];
815
+ return mode === "required" ? defer(event) : [event];
654
816
  };
655
817
 
656
818
  const parseCompletion = (chunks: string[]): string | Error => {
@@ -738,11 +900,15 @@ async function* parseKiroAttempt(
738
900
  if (ev.contextUsagePercentage !== undefined && ev.contextUsagePercentage > 0) {
739
901
  contextUsagePercentage = ev.contextUsagePercentage;
740
902
  }
903
+ if (ev.stopReason !== undefined) stopReason = ev.stopReason;
741
904
  break;
742
905
  case "message_metadata":
743
906
  if (isValidKiroConversationId(ev.conversationId)) returnedConversationId = ev.conversationId;
744
907
  break;
745
908
  case "content":
909
+ if (ev.modelId) {
910
+ contextWindowState.value = kiroUpstreamContextWindow(ev.modelId) ?? contextWindowState.value;
911
+ }
746
912
  if (open) {
747
913
  open = null;
748
914
  return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("content arrived before tool stop")) };
@@ -843,18 +1009,47 @@ async function* parseKiroAttempt(
843
1009
  if (contextUsagePercentage !== undefined) {
844
1010
  debugProviderDiagnostic("kiro", "context_usage", {
845
1011
  contextUsagePercentage,
846
- ...(contextWindow ? { configuredContextWindow: contextWindow } : {}),
1012
+ ...(contextWindowState.value ? { upstreamContextWindow: contextWindowState.value } : {}),
847
1013
  });
848
1014
  }
1015
+ // Kiro's own end-of-turn verdict, trusted only when it is unambiguous: plain assistant text,
1016
+ // with neither a real tool call nor a private completion call to arbitrate against it.
1017
+ // STOP_SEQUENCE is an ordinary native completion just like END_TURN. Both must be decided
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.
1020
+ const normalizedStopReason = stopReason?.trim().toUpperCase();
1021
+ const nativeEndTurn = (normalizedStopReason === KIRO_END_TURN_STOP_REASON
1022
+ || normalizedStopReason === "STOP_SEQUENCE")
1023
+ && sawText
1024
+ && !sawRealTool
1025
+ && completionAnswer === undefined
1026
+ && completionCalls === 0;
1027
+
849
1028
  debugProviderDiagnostic("kiro", "attempt_complete", {
850
1029
  mode,
851
1030
  sawText,
852
1031
  sawReasoning,
853
1032
  sawRealTool,
854
1033
  completionCalls,
1034
+ nativeEndTurn,
1035
+ ...(stopReason !== undefined ? { stopReason } : {}),
855
1036
  assistantChars: assistantText.length,
856
1037
  });
857
1038
 
1039
+ 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
+ for (const event of deferred.splice(0)) yield event;
1051
+ }
1052
+
858
1053
  if (mode === "text_fallback") {
859
1054
  if (completionAnswer !== undefined) {
860
1055
  for (const event of fallbackEvents) yield event;
@@ -917,6 +1112,61 @@ async function* parseKiroAttempt(
917
1112
  terminal: { type: "done", usage: finalUsage, endTurn: false, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
918
1113
  };
919
1114
  }
1115
+ // An explicit native stop reason has already terminated this inference. Converting it into
1116
+ // another model request would hide truncation behind a second paid call, and for context
1117
+ // exhaustion it would resubmit a request that cannot fit. Only a MISSING stop reason falls
1118
+ // through to the bounded compatibility fallback below.
1119
+ //
1120
+ // END_TURN and STOP_SEQUENCE with text are handled earlier by `nativeEndTurn`; reaching
1121
+ // here with either means the turn produced no text.
1122
+ if (mode === "required" && normalizedStopReason !== undefined) {
1123
+ const providerStateField = finalProviderState ? { providerState: finalProviderState } : {};
1124
+ const incomplete = (reason: string, retryable: boolean) => ({
1125
+ assistantText,
1126
+ sawReasoning,
1127
+ terminal: {
1128
+ type: "incomplete" as const,
1129
+ reason,
1130
+ message: `Kiro stopped with ${normalizedStopReason} before an explicit final answer`,
1131
+ usage: finalUsage,
1132
+ retryable,
1133
+ endTurn: false,
1134
+ ...providerStateField,
1135
+ },
1136
+ });
1137
+
1138
+ if (normalizedStopReason === "MODEL_CONTEXT_WINDOW_EXCEEDED") {
1139
+ // Reuse the existing context-length contract (kiro-errors.ts) instead of inventing an
1140
+ // incomplete reason: an unrecognized incomplete becomes a retryable 529 in Claude
1141
+ // outbound, and `max_output_tokens` would make responses/state.ts cache this partial
1142
+ // for continuation replay. Both invite a retry that cannot succeed.
1143
+ return {
1144
+ assistantText,
1145
+ sawReasoning,
1146
+ terminal: {
1147
+ type: "error" as const,
1148
+ message: "Kiro stopped because the model context window was exhausted",
1149
+ status: 400,
1150
+ errorType: "invalid_request_error",
1151
+ code: "context_length_exceeded",
1152
+ retryable: false,
1153
+ usage: finalUsage,
1154
+ },
1155
+ };
1156
+ }
1157
+ if (normalizedStopReason === "MAX_TOKENS") return incomplete("max_output_tokens", true);
1158
+ if (normalizedStopReason === "CONTENT_FILTERED" || normalizedStopReason === "GUARDRAIL_INTERVENED") {
1159
+ return incomplete("content_filter", false);
1160
+ }
1161
+ if (normalizedStopReason === "MALFORMED_TOOL_USE") return incomplete("kiro_malformed_tool_use", false);
1162
+ if (normalizedStopReason === "MALFORMED_MODEL_OUTPUT") return incomplete("kiro_malformed_model_output", false);
1163
+ // TOOL_USE here means Kiro claimed a tool call it never emitted.
1164
+ if (normalizedStopReason === "TOOL_USE") return incomplete("kiro_tool_use_without_call", false);
1165
+ if (normalizedStopReason === KIRO_END_TURN_STOP_REASON || normalizedStopReason === "STOP_SEQUENCE") {
1166
+ return incomplete(`kiro_${normalizedStopReason.toLowerCase()}_without_text`, false);
1167
+ }
1168
+ return incomplete(`kiro_${normalizedStopReason.toLowerCase() || "unknown_stop"}`, false);
1169
+ }
920
1170
  // Kiro text has no trustworthy final/progress marker. When completion is required, ordinary
921
1171
  // text and reasoning remain unfinished until the one bounded fallback validates the turn.
922
1172
  if (mode === "required" && (sawText || sawReasoning)) {
@@ -970,15 +1220,19 @@ export async function* parseKiroStream(
970
1220
  conversationId?: string,
971
1221
  completionMode: KiroCompletionMode = "disabled",
972
1222
  fallbackFactory?: KiroFallbackFactory,
1223
+ contextInputEstimate?: number,
973
1224
  ): AsyncGenerator<AdapterEvent> {
1225
+ const contextWindowState: KiroContextWindowState = { value: contextWindow };
974
1226
  const first = parseKiroAttempt(
975
1227
  response,
976
1228
  completionMode,
977
1229
  modelId,
978
1230
  inputTokens,
979
- contextWindow,
1231
+ contextWindowState,
980
1232
  nameMap,
981
1233
  conversationId,
1234
+ undefined,
1235
+ contextInputEstimate,
982
1236
  );
983
1237
  let firstNext = await first.next();
984
1238
  while (!firstNext.done) {
@@ -1039,10 +1293,11 @@ export async function* parseKiroStream(
1039
1293
  "text_fallback",
1040
1294
  modelId,
1041
1295
  fallback.inputTokens,
1042
- contextWindow,
1296
+ contextWindowState,
1043
1297
  fallback.nameMap,
1044
1298
  fallback.conversationId,
1045
1299
  firstResult.assistantText,
1300
+ fallback.contextInputEstimate,
1046
1301
  );
1047
1302
  let secondNext = await second.next();
1048
1303
  while (!secondNext.done) {
@@ -1054,7 +1309,8 @@ export async function* parseKiroStream(
1054
1309
  yield retryableKiroIncomplete(
1055
1310
  "empty_kiro_fallback",
1056
1311
  "Kiro's bounded completion retry ended without a terminal result",
1057
- mergeKiroUsage(firstResult.usage, secondResult.usage) ?? { inputTokens, outputTokens: 0, estimated: true },
1312
+ mergeKiroUsage(firstResult.usage, secondResult.usage, Boolean(firstResult.assistantText))
1313
+ ?? { inputTokens, outputTokens: 0, estimated: true },
1058
1314
  secondResult.providerState ?? firstResult.providerState,
1059
1315
  );
1060
1316
  return;
@@ -1062,7 +1318,7 @@ export async function* parseKiroStream(
1062
1318
  if (secondResult.terminal.type === "done" || secondResult.terminal.type === "incomplete") {
1063
1319
  yield {
1064
1320
  ...secondResult.terminal,
1065
- usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage),
1321
+ usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, Boolean(firstResult.assistantText)),
1066
1322
  providerState: secondResult.terminal.providerState ?? firstResult.providerState,
1067
1323
  };
1068
1324
  return;
@@ -1070,7 +1326,7 @@ export async function* parseKiroStream(
1070
1326
  yield {
1071
1327
  ...secondResult.terminal,
1072
1328
  ...(secondResult.terminal.type === "error"
1073
- ? { usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage) }
1329
+ ? { usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, Boolean(firstResult.assistantText)) }
1074
1330
  : {}),
1075
1331
  };
1076
1332
  }
@@ -1080,6 +1336,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1080
1336
  // Per-request closure (resolveAdapter builds a fresh adapter per request — server.ts:440 — so this
1081
1337
  // is race-free) carrying the heuristic input-token estimate from buildRequest into the stream.
1082
1338
  let inputTokens = 0;
1339
+ let contextInputEstimate = 0;
1083
1340
  let modelId: string | undefined;
1084
1341
  let contextWindow: number | undefined;
1085
1342
  let toolNameMap: Map<string, string> | undefined;
@@ -1097,6 +1354,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1097
1354
  conversationId: string;
1098
1355
  completionMode: KiroCompletionMode;
1099
1356
  inputTokens: number;
1357
+ contextInputEstimate: number;
1100
1358
  }> => {
1101
1359
  if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
1102
1360
  throw new Error("kiro token missing — run ocx login kiro");
@@ -1118,6 +1376,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1118
1376
  if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
1119
1377
  const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode);
1120
1378
  await normalizeKiroImages(built.payload);
1379
+ const contextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
1121
1380
  const body = JSON.stringify(built.payload);
1122
1381
  debugProviderDiagnostic("kiro", "request", {
1123
1382
  region,
@@ -1141,6 +1400,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1141
1400
  conversationId: built.conversationId,
1142
1401
  completionMode: built.completionMode,
1143
1402
  inputTokens: estimateKiroInputTokens(parsed),
1403
+ contextInputEstimate,
1144
1404
  };
1145
1405
  };
1146
1406
 
@@ -1179,6 +1439,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1179
1439
  return {
1180
1440
  response,
1181
1441
  inputTokens: retry.inputTokens,
1442
+ contextInputEstimate: retry.contextInputEstimate,
1182
1443
  nameMap: retry.nameMap,
1183
1444
  conversationId: retry.conversationId,
1184
1445
  };
@@ -1189,8 +1450,9 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1189
1450
  async buildRequest(parsed: OcxParsedRequest, incoming) {
1190
1451
  const built = await build(parsed);
1191
1452
  modelId = parsed.modelId;
1192
- contextWindow = configuredKiroContextWindow(provider, parsed.modelId);
1453
+ contextWindow = kiroUpstreamContextWindow(parsed.modelId);
1193
1454
  inputTokens = built.inputTokens;
1455
+ contextInputEstimate = built.contextInputEstimate;
1194
1456
  toolNameMap = built.nameMap;
1195
1457
  conversationId = built.conversationId;
1196
1458
  completionMode = built.completionMode;
@@ -1209,6 +1471,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1209
1471
  conversationId,
1210
1472
  completionMode,
1211
1473
  completionMode === "required" ? fallbackFactory : undefined,
1474
+ contextInputEstimate,
1212
1475
  );
1213
1476
  },
1214
1477
 
@@ -1239,6 +1502,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1239
1502
  conversationId,
1240
1503
  completionMode,
1241
1504
  completionMode === "required" ? fallbackFactory : undefined,
1505
+ contextInputEstimate,
1242
1506
  )) events.push(e);
1243
1507
  return events;
1244
1508
  },