@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.
- package/README.md +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/assets/index-BmvM6wRb.js +65 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
- package/gui/dist/assets/index-cydcmbzC.js +0 -52
package/src/adapters/kiro.ts
CHANGED
|
@@ -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
|
|
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),
|
|
205
|
+
return Math.max(estimateKiroInputTokens(parsed), estimateKiroTokens(parts.join("\n"), parsed.modelId));
|
|
159
206
|
}
|
|
160
207
|
|
|
161
|
-
function
|
|
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
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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 = {
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
},
|