@bitkyc08/opencodex 2.7.33 → 2.7.34
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.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +21 -10
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/gui/dist/assets/index-BkmJJgg6.js +52 -0
- package/gui/dist/assets/index-Sg-7L_oZ.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +13 -6
- package/src/adapters/cursor/discovery.ts +39 -4
- package/src/adapters/cursor/exec-policy.ts +11 -13
- package/src/adapters/cursor/live-transport.ts +22 -4
- package/src/adapters/cursor/protobuf-events.ts +140 -8
- package/src/adapters/cursor/protobuf-request.ts +15 -0
- package/src/adapters/cursor/request-builder.ts +10 -5
- package/src/adapters/cursor/transport.ts +3 -2
- package/src/adapters/cursor/types.ts +14 -0
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro-errors.ts +111 -2
- package/src/adapters/kiro-events.ts +154 -35
- package/src/adapters/kiro-retry.ts +116 -32
- package/src/adapters/kiro-tools.ts +30 -20
- package/src/adapters/kiro-wire.ts +47 -6
- package/src/adapters/kiro.ts +891 -228
- package/src/adapters/openai-chat.ts +12 -5
- package/src/adapters/openai-responses.ts +7 -2
- package/src/bridge.ts +109 -26
- package/src/claude/outbound.ts +27 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog.ts +375 -33
- package/src/combos/index.ts +3 -0
- package/src/combos/request.ts +4 -4
- package/src/combos/resolve.ts +2 -2
- package/src/combos/types.ts +104 -2
- package/src/config.ts +70 -1
- package/src/lib/eventstream-decoder.ts +9 -0
- package/src/oauth/index.ts +3 -1
- package/src/oauth/kiro-credentials.ts +48 -20
- package/src/oauth/login-cli.ts +2 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/kiro-models.ts +2 -2
- package/src/providers/openai-sidecar.ts +28 -1
- package/src/providers/registry.ts +39 -2
- package/src/responses/parser.ts +22 -10
- package/src/responses/schema.ts +1 -0
- package/src/responses/state.ts +50 -10
- package/src/router.ts +15 -3
- package/src/server/auth-cors.ts +7 -0
- package/src/server/claude-messages.ts +6 -0
- package/src/server/index.ts +6 -3
- package/src/server/management-api.ts +187 -43
- package/src/server/ports.ts +4 -2
- package/src/server/request-log.ts +3 -2
- package/src/server/responses-item-id-repair.ts +281 -0
- package/src/server/responses.ts +274 -73
- package/src/types.ts +109 -16
- package/src/update/job.ts +81 -1
- package/src/vision/describe.ts +2 -1
- package/src/web-search/executor.ts +2 -1
- package/src/web-search/loop.ts +9 -1
- package/src/web-search/progress-stream.ts +12 -10
- package/gui/dist/assets/index-D6Fcl4yM.css +0 -1
- package/gui/dist/assets/index-d63HMU0x.js +0 -52
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
McpToolResultSchema,
|
|
23
23
|
McpToolsSchema,
|
|
24
24
|
ModelDetailsSchema,
|
|
25
|
+
RequestedModelSchema,
|
|
26
|
+
RequestedModel_ModelParameterbytesSchema,
|
|
25
27
|
ResumeActionSchema,
|
|
26
28
|
RequestContextSchema,
|
|
27
29
|
RequestContextEnvSchema,
|
|
@@ -43,6 +45,9 @@ import {
|
|
|
43
45
|
|
|
44
46
|
const encoder = new TextEncoder();
|
|
45
47
|
|
|
48
|
+
/** Parameter id advertised by Cursor's `default` model for its Cost/Balance/Intelligence control. */
|
|
49
|
+
export const CURSOR_ROUTING_LEVEL_PARAMETER_ID = "optimization";
|
|
50
|
+
|
|
46
51
|
/** Runtime timezone for protobuf RequestContextEnv (dynamic, never hardcoded). */
|
|
47
52
|
function runtimeTimeZone(): string {
|
|
48
53
|
try {
|
|
@@ -364,6 +369,16 @@ export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
|
|
|
364
369
|
displayNameShort: request.modelId,
|
|
365
370
|
aliases: [],
|
|
366
371
|
}),
|
|
372
|
+
...(request.routingLevel ? {
|
|
373
|
+
requestedModel: create(RequestedModelSchema, {
|
|
374
|
+
modelId: request.modelId,
|
|
375
|
+
maxMode: false,
|
|
376
|
+
parameters: [create(RequestedModel_ModelParameterbytesSchema, {
|
|
377
|
+
id: CURSOR_ROUTING_LEVEL_PARAMETER_ID,
|
|
378
|
+
value: request.routingLevel,
|
|
379
|
+
})],
|
|
380
|
+
}),
|
|
381
|
+
} : {}),
|
|
367
382
|
// Mirror the client (Responses) tool definitions into the top-level AgentRunRequest.mcp_tools
|
|
368
383
|
// channel. Advertising them ONLY via native-exec `requestContextArgs` (RequestContext.tools) is
|
|
369
384
|
// insufficient: cursor models report those tools as unavailable and fall back to native tools.
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "../../types";
|
|
9
9
|
import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
|
|
10
10
|
import type { CursorRequestMessage, CursorRunRequest } from "./types";
|
|
11
|
-
import {
|
|
11
|
+
import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
|
|
12
12
|
import { cursorEffortSuffix } from "./effort-map";
|
|
13
13
|
import {
|
|
14
14
|
cursorMcpToolEncodedSize,
|
|
@@ -97,10 +97,11 @@ function catalogLimitNote(kept: readonly OcxTool[], omitted: readonly OcxTool[])
|
|
|
97
97
|
* `undefined` for non-reasoning models like `composer-2.5`. A fully-qualified id (one that isn't a
|
|
98
98
|
* known effort base) passes through unchanged.
|
|
99
99
|
*/
|
|
100
|
-
function normalizeCursorModelId(modelId: string, reasoning?: string): string {
|
|
101
|
-
const
|
|
100
|
+
function normalizeCursorModelId(modelId: string, reasoning?: string): { modelId: string; routingLevel?: CursorRoutingLevel } {
|
|
101
|
+
const selection = cursorWireModelSelection(modelId);
|
|
102
|
+
const id = selection.modelId;
|
|
102
103
|
const suffix = cursorEffortSuffix(id, reasoning);
|
|
103
|
-
return suffix ? `${id}-${suffix}` : id;
|
|
104
|
+
return { ...selection, modelId: suffix ? `${id}-${suffix}` : id };
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined {
|
|
@@ -166,8 +167,10 @@ export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest
|
|
|
166
167
|
const visibleTools = cursorToolsForActivePrompt(parsed.context.tools, activeText, parsed.options.toolChoice);
|
|
167
168
|
const budget = applyCursorToolBudget(visibleTools, parsed.options.toolChoice);
|
|
168
169
|
const limitNote = catalogLimitNote(budget.tools, budget.omitted);
|
|
170
|
+
const model = normalizeCursorModelId(parsed.modelId, parsed.options.reasoning);
|
|
169
171
|
return {
|
|
170
|
-
modelId:
|
|
172
|
+
modelId: model.modelId,
|
|
173
|
+
...(model.routingLevel ? { routingLevel: model.routingLevel } : {}),
|
|
171
174
|
// The Cursor conversation id comes ONLY from remembered state (_cursorConversationId). Do NOT fall
|
|
172
175
|
// back to the OpenAI Responses previous_response_id (resp_*): that is a Responses-chain id in a
|
|
173
176
|
// different namespace and would start an unrelated Cursor conversation, breaking tool-result
|
|
@@ -176,6 +179,8 @@ export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest
|
|
|
176
179
|
system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
|
|
177
180
|
messages,
|
|
178
181
|
rawMessages: parsed.context.messages,
|
|
182
|
+
...(parsed._compactionRequest === true || parsed._contextCompactionBoundary === true ? { contextUsageReset: true } : {}),
|
|
183
|
+
...(parsed._compactionRequest === true ? { contextUsageStoreCheckpoints: false } : {}),
|
|
179
184
|
...(budget.tools.length ? { tools: budget.tools } : {}),
|
|
180
185
|
...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
|
|
181
186
|
...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
|
|
@@ -24,8 +24,9 @@ export interface CursorTransportFactoryInput {
|
|
|
24
24
|
*/
|
|
25
25
|
clientToolFinalizeGraceMs?: number;
|
|
26
26
|
/**
|
|
27
|
-
* True when
|
|
28
|
-
*
|
|
27
|
+
* True when inbound request text carries the legacy Codex full-access sandbox marker.
|
|
28
|
+
* This is retained as diagnostic/context only; exec-policy.ts does not trust it as
|
|
29
|
+
* native local exec authorization because the text is caller-controlled.
|
|
29
30
|
*/
|
|
30
31
|
requestDeclaresFullAccess?: boolean;
|
|
31
32
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { OcxUsage } from "../../types";
|
|
2
2
|
import type { OcxMessage, OcxRequestOptions, OcxTool } from "../../types";
|
|
3
|
+
import type { CursorRoutingLevel } from "./discovery";
|
|
3
4
|
|
|
4
5
|
export interface CursorRunRequest {
|
|
5
6
|
modelId: string;
|
|
7
|
+
/** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
|
|
8
|
+
routingLevel?: CursorRoutingLevel;
|
|
6
9
|
conversationId: string;
|
|
7
10
|
system: string[];
|
|
8
11
|
messages: CursorRequestMessage[];
|
|
@@ -10,6 +13,17 @@ export interface CursorRunRequest {
|
|
|
10
13
|
tools?: OcxTool[];
|
|
11
14
|
toolChoice?: OcxRequestOptions["toolChoice"];
|
|
12
15
|
parallelToolCalls?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Clear provider-private context-usage carry-forward before this run. Used when Codex starts a
|
|
18
|
+
* newly observed compacted context epoch, so pre-compaction totals are not over-reported while
|
|
19
|
+
* historical previous_response_id replay remains idempotent.
|
|
20
|
+
*/
|
|
21
|
+
contextUsageReset?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Defaults to true. Set false for compaction summarizer turns: their checkpoints describe the
|
|
24
|
+
* pre-compaction history being summarized and must not become the next turn's carry-forward total.
|
|
25
|
+
*/
|
|
26
|
+
contextUsageStoreCheckpoints?: boolean;
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
export interface CursorRequestMessage {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const KIRO_COMPLETION_TOOL_NAME = "codex_kiro_final_answer";
|
|
2
|
+
export const KIRO_CONTINUATION_MESSAGE = "[system: conversation continues]";
|
|
3
|
+
export const KIRO_COMPLETION_RETRY_MESSAGE =
|
|
4
|
+
`[system: The preceding assistant output did not explicitly complete the turn. If the task is complete, call ${KIRO_COMPLETION_TOOL_NAME} now with the complete final answer. Otherwise issue the next real tool call now. Do not ask the user for another task or emit another progress-only message.]`;
|
|
5
|
+
|
|
6
|
+
export const KIRO_COMPLETION_INSTRUCTIONS =
|
|
7
|
+
`When tools are available, ordinary assistant text is mid-task commentary and does not end the turn. Continue using tools after progress updates. When the task is fully complete and no more tool calls are needed, call ${KIRO_COMPLETION_TOOL_NAME} exactly once with the complete user-facing final answer in \`answer\`. Do not provide the final answer as ordinary assistant text.`;
|
|
8
|
+
|
|
9
|
+
export type KiroCompletionMode = "disabled" | "required" | "text_fallback";
|
|
10
|
+
|
|
11
|
+
/** Bound proxy-authored prompt additions independently of caller-owned instructions/history. */
|
|
12
|
+
export const MAX_KIRO_INJECTED_INSTRUCTION_CHARS = 16_384;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error";
|
|
2
|
-
const DETAIL_KEYS = ["__type", "code", "error", "name", "message", "Message", "errorMessage"];
|
|
2
|
+
const DETAIL_KEYS = ["__type", "code", "error", "name", "reason", "message", "Message", "errorMessage"];
|
|
3
|
+
|
|
4
|
+
export interface KiroErrorClassification {
|
|
5
|
+
message: string;
|
|
6
|
+
status: number;
|
|
7
|
+
errorType: string;
|
|
8
|
+
code: string;
|
|
9
|
+
retryable: boolean;
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
function headerValue(headers: Headers | Record<string, unknown>, name: string): string | undefined {
|
|
5
13
|
if (headers instanceof Headers) return name.startsWith(":") ? undefined : safeUpstreamErrorString(headers.get(name));
|
|
@@ -79,10 +87,111 @@ function normalizedKiroErrorMessage(headers: Headers | Record<string, unknown>,
|
|
|
79
87
|
return detail ? `${prefix}: ${detail}` : prefix;
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
function isContentLengthError(text: string): boolean {
|
|
91
|
+
const lower = text.toLowerCase();
|
|
92
|
+
return lower.includes("content_length_exceeds_threshold") || lower.includes("content length exceeds");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function classifyKiroFailure(
|
|
96
|
+
headers: Headers | Record<string, unknown>,
|
|
97
|
+
payloadText: string,
|
|
98
|
+
status?: number,
|
|
99
|
+
): KiroErrorClassification {
|
|
100
|
+
const message = normalizedKiroErrorMessage(headers, payloadText, status);
|
|
101
|
+
const headerType = headerValue(headers, ":exception-type") || headerValue(headers, ":error-type") || "";
|
|
102
|
+
const evidence = [headerType, ...payloadDetails(payloadText), message].join(" ").toLowerCase();
|
|
103
|
+
if (isContentLengthError(evidence)) {
|
|
104
|
+
return {
|
|
105
|
+
message: "Kiro rejected the request because the conversation exceeds the model's context window. Compact or reduce the history, or start a new session.",
|
|
106
|
+
status: 400,
|
|
107
|
+
errorType: "invalid_request_error",
|
|
108
|
+
code: "context_length_exceeded",
|
|
109
|
+
retryable: false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
evidence.includes("insufficient_quota")
|
|
114
|
+
|| evidence.includes("quota exhausted")
|
|
115
|
+
|| evidence.includes("quota exceeded")
|
|
116
|
+
) {
|
|
117
|
+
return { message, status: 429, errorType: "insufficient_quota", code: "insufficient_quota", retryable: false };
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
status === 429
|
|
121
|
+
|| evidence.includes("throttlingexception")
|
|
122
|
+
|| evidence.includes("too many requests")
|
|
123
|
+
|| evidence.includes("rate limit")
|
|
124
|
+
) {
|
|
125
|
+
return { message, status: 429, errorType: "rate_limit_error", code: "rate_limit_exceeded", retryable: true };
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
status === 401
|
|
129
|
+
|| status === 403
|
|
130
|
+
|| evidence.includes("accessdenied")
|
|
131
|
+
|| evidence.includes("unauthorized")
|
|
132
|
+
|| evidence.includes("unrecognizedclient")
|
|
133
|
+
|| evidence.includes("expiredtoken")
|
|
134
|
+
|| evidence.includes("expired token")
|
|
135
|
+
|| evidence.includes("invalid token")
|
|
136
|
+
|| evidence.includes("authentication")
|
|
137
|
+
) {
|
|
138
|
+
return { message, status: status === 403 ? 403 : 401, errorType: status === 403 ? "permission_error" : "authentication_error", code: status === 403 ? "permission_denied" : "invalid_api_key", retryable: false };
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
status === 400
|
|
142
|
+
|| evidence.includes("validationexception")
|
|
143
|
+
|| evidence.includes("invalid request")
|
|
144
|
+
|| evidence.includes("model unavailable")
|
|
145
|
+
|| evidence.includes("model not found")
|
|
146
|
+
|| evidence.includes("unsupported model")
|
|
147
|
+
|| evidence.includes("profile arn")
|
|
148
|
+
|| evidence.includes("malformed")
|
|
149
|
+
) {
|
|
150
|
+
return { message, status: 400, errorType: "invalid_request_error", code: "invalid_request_error", retryable: false };
|
|
151
|
+
}
|
|
152
|
+
if (
|
|
153
|
+
status === 503
|
|
154
|
+
|| evidence.includes("overloaded")
|
|
155
|
+
|| evidence.includes("server is busy")
|
|
156
|
+
|| evidence.includes("temporarily unavailable")
|
|
157
|
+
) {
|
|
158
|
+
return { message, status: 503, errorType: "server_error", code: "server_is_overloaded", retryable: true };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
message,
|
|
162
|
+
status: status && status >= 500 ? status : 502,
|
|
163
|
+
errorType: "server_error",
|
|
164
|
+
code: "upstream_server_error",
|
|
165
|
+
retryable: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
82
169
|
export function safeKiroErrorMessage(headers: Record<string, unknown>, payloadText: string): string {
|
|
83
170
|
return normalizedKiroErrorMessage(headers, payloadText);
|
|
84
171
|
}
|
|
85
172
|
|
|
173
|
+
export function classifyKiroStreamError(
|
|
174
|
+
headers: Record<string, unknown>,
|
|
175
|
+
payloadText: string,
|
|
176
|
+
): KiroErrorClassification {
|
|
177
|
+
return classifyKiroFailure(headers, payloadText);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function classifyKiroHttpError(
|
|
181
|
+
status: number,
|
|
182
|
+
headers: Headers | Record<string, unknown>,
|
|
183
|
+
payloadText: string,
|
|
184
|
+
): KiroErrorClassification {
|
|
185
|
+
return classifyKiroFailure(headers, payloadText, status);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function classifyKiroEventError(reason: string | undefined, message: string | undefined): KiroErrorClassification {
|
|
189
|
+
const safeReason = reason ? sanitizeUpstreamErrorText(reason).slice(0, 160) : "";
|
|
190
|
+
const safeMessage = message ? sanitizeUpstreamErrorText(message).slice(0, 500) : "Kiro request failed";
|
|
191
|
+
const payload = JSON.stringify({ reason: safeReason, message: safeMessage });
|
|
192
|
+
return classifyKiroFailure({}, payload);
|
|
193
|
+
}
|
|
194
|
+
|
|
86
195
|
export function safeKiroHttpErrorMessage(status: number, headers: Headers | Record<string, unknown>, payloadText: string): string {
|
|
87
|
-
return
|
|
196
|
+
return classifyKiroFailure(headers, payloadText, status).message;
|
|
88
197
|
}
|
|
@@ -1,48 +1,167 @@
|
|
|
1
|
+
import type { OcxUsage } from "../types";
|
|
1
2
|
import { kiroTruncationReason } from "./kiro-truncation";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
type: "content"
|
|
5
|
-
data?: string
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
export type ParsedKiroEvent =
|
|
5
|
+
| { type: "content"; data?: string; modelId?: string }
|
|
6
|
+
| { type: "reasoning"; data?: string }
|
|
7
|
+
| { type: "tool"; name?: string; toolUseId?: string; input?: string; stop?: boolean }
|
|
8
|
+
| { type: "truncation"; data: string }
|
|
9
|
+
| { type: "metadata"; usage?: OcxUsage; contextUsagePercentage?: number }
|
|
10
|
+
| { type: "message_metadata"; conversationId?: string }
|
|
11
|
+
| { type: "invalid_state"; message?: string }
|
|
12
|
+
| { type: "error"; reason?: string; message?: string };
|
|
13
|
+
|
|
14
|
+
const KNOWN_EVENT_TYPES = new Set([
|
|
15
|
+
"assistantResponseEvent",
|
|
16
|
+
"reasoningContentEvent",
|
|
17
|
+
"toolUseEvent",
|
|
18
|
+
"messageMetadataEvent",
|
|
19
|
+
"metadataEvent",
|
|
20
|
+
"invalidStateEvent",
|
|
21
|
+
"error",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function malformed(eventType: string, detail: string): never {
|
|
25
|
+
throw new Error(`invalid Kiro ${eventType} payload: ${detail}`);
|
|
11
26
|
}
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
let
|
|
28
|
+
function parseObject(eventType: string, payload: Uint8Array): Record<string, unknown> {
|
|
29
|
+
let value: unknown;
|
|
15
30
|
try {
|
|
16
|
-
|
|
31
|
+
value = JSON.parse(new TextDecoder().decode(payload));
|
|
17
32
|
} catch {
|
|
18
|
-
return
|
|
33
|
+
return malformed(eventType, "expected valid JSON");
|
|
19
34
|
}
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
36
|
+
return malformed(eventType, "expected an object");
|
|
37
|
+
}
|
|
38
|
+
return value as Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function optionalString(eventType: string, obj: Record<string, unknown>, key: string): string | undefined {
|
|
42
|
+
const value = obj[key];
|
|
43
|
+
if (value === undefined || value === null) return undefined;
|
|
44
|
+
if (typeof value !== "string") return malformed(eventType, `${key} must be a string`);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function optionalBoolean(eventType: string, obj: Record<string, unknown>, key: string): boolean | undefined {
|
|
49
|
+
const value = obj[key];
|
|
50
|
+
if (value === undefined || value === null) return undefined;
|
|
51
|
+
if (typeof value !== "boolean") return malformed(eventType, `${key} must be a boolean`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tokenCount(eventType: string, obj: Record<string, unknown>, key: string, required: boolean): number {
|
|
56
|
+
const value = obj[key];
|
|
57
|
+
if (value === undefined && !required) return 0;
|
|
58
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
59
|
+
return malformed(eventType, `${key} must be a non-negative safe integer`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseTokenUsage(eventType: string, value: unknown): OcxUsage | undefined {
|
|
65
|
+
if (value === undefined || value === null) return undefined;
|
|
66
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
67
|
+
return malformed(eventType, "tokenUsage must be an object");
|
|
26
68
|
}
|
|
69
|
+
const usage = value as Record<string, unknown>;
|
|
70
|
+
const uncached = tokenCount(eventType, usage, "uncachedInputTokens", true);
|
|
71
|
+
const cacheRead = tokenCount(eventType, usage, "cacheReadInputTokens", false);
|
|
72
|
+
const cacheWrite = tokenCount(eventType, usage, "cacheWriteInputTokens", false);
|
|
73
|
+
const outputTokens = tokenCount(eventType, usage, "outputTokens", true);
|
|
74
|
+
const totalTokens = tokenCount(eventType, usage, "totalTokens", true);
|
|
75
|
+
const inputTokens = uncached + cacheRead + cacheWrite;
|
|
76
|
+
if (!Number.isSafeInteger(inputTokens)) return malformed(eventType, "input token usage overflowed");
|
|
77
|
+
return {
|
|
78
|
+
inputTokens,
|
|
79
|
+
outputTokens,
|
|
80
|
+
totalTokens,
|
|
81
|
+
cachedInputTokens: cacheRead,
|
|
82
|
+
cacheReadInputTokens: cacheRead,
|
|
83
|
+
cacheCreationInputTokens: cacheWrite,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Decode a known Kiro event using its Smithy `:event-type` header. */
|
|
88
|
+
export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKiroEvent | null {
|
|
89
|
+
// Unknown event types are intentionally ignored without parsing or logging their payload.
|
|
90
|
+
if (!KNOWN_EVENT_TYPES.has(eventType)) return null;
|
|
91
|
+
const parsed = parseObject(eventType, payload);
|
|
27
92
|
const truncationReason = kiroTruncationReason(parsed);
|
|
28
93
|
if (truncationReason) return { type: "truncation", data: truncationReason };
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
|
|
95
|
+
switch (eventType) {
|
|
96
|
+
case "assistantResponseEvent":
|
|
97
|
+
return {
|
|
98
|
+
type: "content",
|
|
99
|
+
...(optionalString(eventType, parsed, "content") !== undefined
|
|
100
|
+
? { data: optionalString(eventType, parsed, "content") }
|
|
101
|
+
: {}),
|
|
102
|
+
...(optionalString(eventType, parsed, "modelId") !== undefined
|
|
103
|
+
? { modelId: optionalString(eventType, parsed, "modelId") }
|
|
104
|
+
: {}),
|
|
105
|
+
};
|
|
106
|
+
case "reasoningContentEvent":
|
|
107
|
+
return {
|
|
108
|
+
type: "reasoning",
|
|
109
|
+
...(optionalString(eventType, parsed, "text") !== undefined
|
|
110
|
+
? { data: optionalString(eventType, parsed, "text") }
|
|
111
|
+
: {}),
|
|
112
|
+
};
|
|
113
|
+
case "toolUseEvent":
|
|
114
|
+
return {
|
|
115
|
+
type: "tool",
|
|
116
|
+
...(optionalString(eventType, parsed, "name") !== undefined
|
|
117
|
+
? { name: optionalString(eventType, parsed, "name") }
|
|
118
|
+
: {}),
|
|
119
|
+
...(optionalString(eventType, parsed, "toolUseId") !== undefined
|
|
120
|
+
? { toolUseId: optionalString(eventType, parsed, "toolUseId") }
|
|
121
|
+
: {}),
|
|
122
|
+
...(optionalString(eventType, parsed, "input") !== undefined
|
|
123
|
+
? { input: optionalString(eventType, parsed, "input") }
|
|
124
|
+
: {}),
|
|
125
|
+
...(optionalBoolean(eventType, parsed, "stop") !== undefined
|
|
126
|
+
? { stop: optionalBoolean(eventType, parsed, "stop") }
|
|
127
|
+
: {}),
|
|
128
|
+
};
|
|
129
|
+
case "messageMetadataEvent":
|
|
130
|
+
return {
|
|
131
|
+
type: "message_metadata",
|
|
132
|
+
conversationId:
|
|
133
|
+
optionalString(eventType, parsed, "conversationId")
|
|
134
|
+
?? optionalString(eventType, parsed, "utteranceId"),
|
|
135
|
+
};
|
|
136
|
+
case "metadataEvent": {
|
|
137
|
+
const contextUsagePercentage = parsed.contextUsagePercentage;
|
|
138
|
+
if (
|
|
139
|
+
contextUsagePercentage !== undefined
|
|
140
|
+
&& (typeof contextUsagePercentage !== "number" || !Number.isFinite(contextUsagePercentage))
|
|
141
|
+
) {
|
|
142
|
+
return malformed(eventType, "contextUsagePercentage must be a finite number");
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
type: "metadata",
|
|
146
|
+
...(parseTokenUsage(eventType, parsed.tokenUsage) !== undefined
|
|
147
|
+
? { usage: parseTokenUsage(eventType, parsed.tokenUsage) }
|
|
148
|
+
: {}),
|
|
149
|
+
...(typeof contextUsagePercentage === "number" ? { contextUsagePercentage } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
case "invalidStateEvent":
|
|
153
|
+
return { type: "invalid_state", message: optionalString(eventType, parsed, "message") };
|
|
154
|
+
case "error":
|
|
155
|
+
return {
|
|
156
|
+
type: "error",
|
|
157
|
+
reason:
|
|
158
|
+
optionalString(eventType, parsed, "reason")
|
|
159
|
+
?? optionalString(eventType, parsed, "type")
|
|
160
|
+
?? optionalString(eventType, parsed, "__type"),
|
|
161
|
+
message:
|
|
162
|
+
optionalString(eventType, parsed, "message")
|
|
163
|
+
?? optionalString(eventType, parsed, "Message"),
|
|
164
|
+
};
|
|
45
165
|
}
|
|
46
|
-
if (name !== undefined) return { type: "tool_start", name, toolUseId };
|
|
47
166
|
return null;
|
|
48
167
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
2
2
|
import { safeKiroHttpErrorMessage } from "./kiro-errors";
|
|
3
3
|
import { normalizeUpstreamHttpErrorResponse } from "./upstream-http-error";
|
|
4
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
4
5
|
import {
|
|
5
6
|
abortError,
|
|
6
7
|
cancelResponseBodyBestEffort,
|
|
@@ -10,54 +11,137 @@ import {
|
|
|
10
11
|
sleepWithAbort,
|
|
11
12
|
} from "../lib/upstream-retry";
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const RESET_ATTEMPTS = 3;
|
|
15
|
+
const RESET_RETRY_BASE_MS = 150;
|
|
16
|
+
const RESET_RETRY_MAX_MS = 1_000;
|
|
17
|
+
const CANONICAL_RUNTIME_HOST = /^runtime\.([a-z]{2}(?:-[a-z]+)+-\d)\.kiro\.dev$/i;
|
|
18
|
+
const ENDPOINT_ERROR_MARKERS = [
|
|
19
|
+
"unknownoperation",
|
|
20
|
+
"unknown operation",
|
|
21
|
+
"invalidsignature",
|
|
22
|
+
"invalid signature",
|
|
23
|
+
"endpoint not found",
|
|
24
|
+
"unsupported endpoint",
|
|
25
|
+
];
|
|
26
|
+
const CONNECT_ERROR_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH"]);
|
|
16
27
|
|
|
17
|
-
function
|
|
18
|
-
|
|
28
|
+
function errorChain(error: unknown): Error[] {
|
|
29
|
+
const chain: Error[] = [];
|
|
30
|
+
let current = error;
|
|
31
|
+
const seen = new Set<unknown>();
|
|
32
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
33
|
+
seen.add(current);
|
|
34
|
+
chain.push(current);
|
|
35
|
+
current = (current as Error & { cause?: unknown }).cause;
|
|
36
|
+
}
|
|
37
|
+
return chain;
|
|
19
38
|
}
|
|
20
39
|
|
|
21
|
-
function
|
|
22
|
-
return
|
|
40
|
+
function endpointConnectFailure(error: unknown): boolean {
|
|
41
|
+
return errorChain(error).some(item => {
|
|
42
|
+
if (item.name === "AbortError" || item.name === "TimeoutError") return false;
|
|
43
|
+
const code = (item as Error & { code?: unknown }).code;
|
|
44
|
+
if (typeof code === "string" && CONNECT_ERROR_CODES.has(code)) return true;
|
|
45
|
+
const message = item.message.toLowerCase();
|
|
46
|
+
return message.includes("dns")
|
|
47
|
+
|| message.includes("name resolution")
|
|
48
|
+
|| message.includes("failed to lookup address")
|
|
49
|
+
|| message.includes("connection refused")
|
|
50
|
+
|| message.includes("failed to connect")
|
|
51
|
+
|| message.includes("connect error");
|
|
52
|
+
});
|
|
23
53
|
}
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
function legacyUrl(requestUrl: string): string | undefined {
|
|
56
|
+
let url: URL;
|
|
57
|
+
try { url = new URL(requestUrl); } catch { return undefined; }
|
|
58
|
+
const match = CANONICAL_RUNTIME_HOST.exec(url.hostname);
|
|
59
|
+
if (!match || url.pathname !== "/" || url.search || url.hash) return undefined;
|
|
60
|
+
url.hostname = `q.${match[1]}.amazonaws.com`;
|
|
61
|
+
return url.toString();
|
|
30
62
|
}
|
|
31
63
|
|
|
32
|
-
|
|
33
|
-
|
|
64
|
+
async function fetchWithResetRecovery(
|
|
65
|
+
request: AdapterRequest,
|
|
66
|
+
url: string,
|
|
67
|
+
ctx: AdapterFetchContext,
|
|
68
|
+
timeoutMs: number,
|
|
69
|
+
): Promise<Response> {
|
|
34
70
|
let lastError: unknown;
|
|
35
|
-
for (let attempt = 0; attempt <
|
|
71
|
+
for (let attempt = 0; attempt < RESET_ATTEMPTS; attempt++) {
|
|
36
72
|
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
37
73
|
try {
|
|
38
|
-
const
|
|
74
|
+
const headers = new Headers(request.headers);
|
|
75
|
+
const recovered = attempt > 0;
|
|
76
|
+
if (recovered) headers.set("connection", "close");
|
|
77
|
+
return await fetchWithAttemptDeadline(url, {
|
|
39
78
|
method: request.method,
|
|
40
|
-
headers
|
|
79
|
+
headers,
|
|
41
80
|
body: request.body,
|
|
81
|
+
...(recovered ? { keepalive: false } : {}),
|
|
42
82
|
}, timeoutMs, ctx.abortSignal, ctx.stream);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
cancelResponseBodyBestEffort(res);
|
|
47
|
-
await sleepWithAbort(retryBackoffDelayMs(attempt, {
|
|
48
|
-
baseDelayMs: KIRO_RETRY_BASE_MS,
|
|
49
|
-
maxDelayMs: KIRO_RETRY_MAX_MS,
|
|
50
|
-
headers: res.headers,
|
|
51
|
-
}), ctx.abortSignal);
|
|
52
|
-
} catch (err) {
|
|
53
|
-
if (ctx.abortSignal?.aborted) throw err;
|
|
54
|
-
if (!retryableKiroFetchError(err) || attempt === KIRO_RETRY_ATTEMPTS - 1) throw err;
|
|
55
|
-
lastError = err;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (ctx.abortSignal?.aborted || !isConnectionResetError(error) || attempt === RESET_ATTEMPTS - 1) throw error;
|
|
85
|
+
lastError = error;
|
|
56
86
|
await sleepWithAbort(retryBackoffDelayMs(attempt, {
|
|
57
|
-
baseDelayMs:
|
|
58
|
-
maxDelayMs:
|
|
87
|
+
baseDelayMs: RESET_RETRY_BASE_MS,
|
|
88
|
+
maxDelayMs: RESET_RETRY_MAX_MS,
|
|
59
89
|
}), ctx.abortSignal);
|
|
60
90
|
}
|
|
61
91
|
}
|
|
62
92
|
throw lastError ?? new Error("Kiro fetch failed");
|
|
63
93
|
}
|
|
94
|
+
|
|
95
|
+
async function inspectEndpointHttpFailure(
|
|
96
|
+
response: Response,
|
|
97
|
+
signal?: AbortSignal,
|
|
98
|
+
): Promise<{ response: Response; fallback: boolean }> {
|
|
99
|
+
if (response.status === 404 || response.status === 405) return { response, fallback: true };
|
|
100
|
+
if (response.status !== 400 && response.status !== 403) return { response, fallback: false };
|
|
101
|
+
|
|
102
|
+
const body = await readBoundedResponseBody(response, { signal });
|
|
103
|
+
const headers = new Headers(response.headers);
|
|
104
|
+
headers.delete("content-encoding");
|
|
105
|
+
headers.delete("content-length");
|
|
106
|
+
const rebuilt = new Response(body.displaySafe ? body.text : "", {
|
|
107
|
+
status: response.status,
|
|
108
|
+
statusText: response.statusText,
|
|
109
|
+
headers,
|
|
110
|
+
});
|
|
111
|
+
const text = body.displaySafe ? body.text.toLowerCase() : "";
|
|
112
|
+
return { response: rebuilt, fallback: ENDPOINT_ERROR_MARKERS.some(marker => text.includes(marker)) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function normalizeFinalKiroHttpError(res: Response, signal?: AbortSignal): Promise<Response> {
|
|
116
|
+
return normalizeUpstreamHttpErrorResponse(res, {
|
|
117
|
+
signal,
|
|
118
|
+
formatMessage: payloadText => safeKiroHttpErrorMessage(res.status, res.headers, payloadText),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Kiro owns only replay-safe pre-header reset recovery and one canonical-to-legacy endpoint fallback.
|
|
124
|
+
* Client-level retry policy owns throttling, timeouts, and ordinary service failures.
|
|
125
|
+
*/
|
|
126
|
+
export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
127
|
+
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
128
|
+
const legacy = legacyUrl(request.url);
|
|
129
|
+
let response: Response;
|
|
130
|
+
try {
|
|
131
|
+
response = await fetchWithResetRecovery(request, request.url, ctx, timeoutMs);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (!legacy || !endpointConnectFailure(error)) throw error;
|
|
134
|
+
response = await fetchWithResetRecovery(request, legacy, ctx, timeoutMs);
|
|
135
|
+
return ctx.returnRawErrors ? response : normalizeFinalKiroHttpError(response, ctx.abortSignal);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (legacy && !response.ok) {
|
|
139
|
+
const inspected = await inspectEndpointHttpFailure(response, ctx.abortSignal);
|
|
140
|
+
response = inspected.response;
|
|
141
|
+
if (inspected.fallback) {
|
|
142
|
+
cancelResponseBodyBestEffort(response);
|
|
143
|
+
response = await fetchWithResetRecovery(request, legacy, ctx, timeoutMs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return ctx.returnRawErrors ? response : normalizeFinalKiroHttpError(response, ctx.abortSignal);
|
|
147
|
+
}
|