@bitkyc08/opencodex 2.6.1 → 2.6.2
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 +8 -0
- package/bin/ocx.mjs +41 -9
- package/gui/dist/assets/index-LK87QnT7.js +9 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/abort.ts +22 -0
- package/src/adapters/base.ts +17 -4
- package/src/adapters/kiro-errors.ts +101 -0
- package/src/adapters/kiro-events.ts +48 -0
- package/src/adapters/kiro-images.ts +33 -0
- package/src/adapters/kiro-retry.ts +95 -0
- package/src/adapters/kiro-thinking.ts +82 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +44 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +51 -0
- package/src/adapters/kiro.ts +527 -0
- package/src/adapters/openai-chat.ts +10 -1
- package/src/bridge.ts +1 -1
- package/src/cli.ts +25 -3
- package/src/codex-catalog.ts +97 -13
- package/src/codex-inject.ts +18 -0
- package/src/config.ts +52 -0
- package/src/crash-guard.ts +197 -9
- package/src/debug.ts +11 -0
- package/src/errors.ts +39 -3
- package/src/lib/eventstream-decoder.ts +244 -0
- package/src/lib/token-estimate.ts +43 -0
- package/src/oauth/anthropic.ts +1 -1
- package/src/oauth/index.ts +53 -6
- package/src/oauth/kiro-credentials.ts +256 -0
- package/src/oauth/kiro.ts +164 -0
- package/src/oauth/local-token-detect.ts +2 -1
- package/src/oauth/store.ts +36 -3
- package/src/oauth/types.ts +3 -0
- package/src/oauth/xai.ts +1 -1
- package/src/providers/kiro-models.ts +55 -0
- package/src/providers/registry.ts +15 -0
- package/src/redact.ts +71 -0
- package/src/server.ts +40 -22
- package/src/sidecar-tracker.ts +49 -0
- package/src/types.ts +3 -0
- package/src/usage-debug.ts +7 -4
- package/src/usage-log.ts +41 -3
- package/src/vision/describe.ts +11 -2
- package/src/web-search/executor.ts +10 -2
- package/src/web-search/loop.ts +27 -7
- package/gui/dist/assets/index-BmHrbTmO.js +0 -9
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { decodeEventStream } from "../lib/eventstream-decoder";
|
|
2
|
+
import { estimateTokens } from "../lib/token-estimate";
|
|
3
|
+
import { debugProviderDiagnostic } from "../debug";
|
|
4
|
+
import { resolveKiroApiRegion, resolveKiroProfileArn } from "../oauth/kiro";
|
|
5
|
+
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
|
|
6
|
+
import { modelRecordValue } from "../reasoning-effort";
|
|
7
|
+
import { parseKiroEvent } from "./kiro-events";
|
|
8
|
+
import { safeKiroErrorMessage } from "./kiro-errors";
|
|
9
|
+
import { appendFallbackText, toolCallFallbackText, toolResultFallbackText } from "./kiro-tool-fallback";
|
|
10
|
+
import { KiroThinkingParser } from "./kiro-thinking";
|
|
11
|
+
import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "./kiro-truncation";
|
|
12
|
+
import { fallbackToolUseId, fingerprint, invocationId, mapModelId, normalizeToolId, osTag, stableConversationId } from "./kiro-wire";
|
|
13
|
+
import type {
|
|
14
|
+
AdapterEvent,
|
|
15
|
+
OcxAssistantMessage,
|
|
16
|
+
OcxContentPart,
|
|
17
|
+
OcxMessage,
|
|
18
|
+
OcxParsedRequest,
|
|
19
|
+
OcxProviderConfig,
|
|
20
|
+
OcxTextContent,
|
|
21
|
+
OcxToolCall,
|
|
22
|
+
OcxToolResultMessage,
|
|
23
|
+
OcxUsage,
|
|
24
|
+
} from "../types";
|
|
25
|
+
import type { ProviderAdapter } from "./base";
|
|
26
|
+
import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
27
|
+
import { extractKiroImages, type KiroImage } from "./kiro-images";
|
|
28
|
+
import { fetchKiroWithRetry } from "./kiro-retry";
|
|
29
|
+
import { convertKiroToolContext } from "./kiro-tools";
|
|
30
|
+
|
|
31
|
+
const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse";
|
|
32
|
+
const SDK_VERSION = "1.0.27";
|
|
33
|
+
const NODE_VERSION = "22.21.1";
|
|
34
|
+
const KIRO_IDE_VERSION = "1.2.0";
|
|
35
|
+
|
|
36
|
+
// Payload construction (conversationState)
|
|
37
|
+
interface KiroToolUse {
|
|
38
|
+
name: string;
|
|
39
|
+
input: Record<string, unknown>; // OBJECT, not stringified
|
|
40
|
+
toolUseId: string;
|
|
41
|
+
}
|
|
42
|
+
interface KiroToolResult {
|
|
43
|
+
content: Array<{ text: string }>;
|
|
44
|
+
status: string;
|
|
45
|
+
toolUseId: string;
|
|
46
|
+
}
|
|
47
|
+
interface KiroUserInputMessage {
|
|
48
|
+
content: string;
|
|
49
|
+
modelId?: string;
|
|
50
|
+
origin?: string;
|
|
51
|
+
userInputMessageContext?: { tools?: unknown[]; toolResults?: KiroToolResult[] };
|
|
52
|
+
images?: KiroImage[];
|
|
53
|
+
}
|
|
54
|
+
interface KiroHistoryEntry {
|
|
55
|
+
userInputMessage?: KiroUserInputMessage;
|
|
56
|
+
assistantResponseMessage?: { content: string; toolUses?: KiroToolUse[] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function userContentText(content: string | OcxContentPart[]): string {
|
|
60
|
+
if (typeof content === "string") return content;
|
|
61
|
+
return content.map(p => (p.type === "text" ? p.text : "")).filter(Boolean).join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function usageContentText(content: string | OcxContentPart[]): string {
|
|
65
|
+
if (typeof content === "string") return content;
|
|
66
|
+
return content
|
|
67
|
+
.map(p => {
|
|
68
|
+
if (p.type === "text") return p.text;
|
|
69
|
+
if (p.type === "image") return `[image:${p.detail ?? "auto"}]`;
|
|
70
|
+
return "";
|
|
71
|
+
})
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function serializeForUsage(value: unknown): string {
|
|
77
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function currentTurnUsageMessages(messages: OcxMessage[]): OcxMessage[] {
|
|
81
|
+
return messages.slice(messages.map(m => m.role).lastIndexOf("assistant") + 1).filter(m => m.role !== "assistant");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function currentTurnPayloadMessages(messages: OcxMessage[]): OcxMessage[] {
|
|
85
|
+
const roles = messages.map(m => m.role);
|
|
86
|
+
const lastAssistant = roles.lastIndexOf("assistant");
|
|
87
|
+
const tail = messages.slice(lastAssistant + 1).filter(m => m.role !== "assistant");
|
|
88
|
+
if (lastAssistant === -1 || !tail.some(m => m.role === "toolResult")) return tail;
|
|
89
|
+
const priorAssistant = roles.slice(0, lastAssistant).lastIndexOf("assistant");
|
|
90
|
+
return messages.slice(priorAssistant + 1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function kiroPayloadMessages(parsed: OcxParsedRequest): OcxMessage[] {
|
|
94
|
+
return parsed.previousResponseId ? currentTurnPayloadMessages(parsed.context.messages) : parsed.context.messages;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function messageUsageText(msg: OcxMessage): string {
|
|
98
|
+
switch (msg.role) {
|
|
99
|
+
case "user":
|
|
100
|
+
case "developer":
|
|
101
|
+
return usageContentText(msg.content);
|
|
102
|
+
case "toolResult":
|
|
103
|
+
return [
|
|
104
|
+
msg.toolName,
|
|
105
|
+
msg.toolCallId,
|
|
106
|
+
msg.isError ? "error" : "success",
|
|
107
|
+
usageContentText(msg.content),
|
|
108
|
+
].filter(Boolean).join("\n");
|
|
109
|
+
case "assistant":
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function messageLogText(msg: OcxMessage): string {
|
|
115
|
+
if (msg.role !== "assistant") return messageUsageText(msg);
|
|
116
|
+
return msg.content.map(part => {
|
|
117
|
+
if (part.type === "text") return part.text;
|
|
118
|
+
if (part.type === "toolCall") return [part.name, part.id, serializeForUsage(part.arguments)].join("\n");
|
|
119
|
+
return part.thinking;
|
|
120
|
+
}).filter(Boolean).join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function shouldCountStablePromptOverhead(parsed: OcxParsedRequest): boolean {
|
|
124
|
+
return !parsed.previousResponseId && !parsed.context.messages.some(m => m.role === "assistant");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function estimateKiroInputTokens(parsed: OcxParsedRequest): number {
|
|
128
|
+
const parts = currentTurnUsageMessages(parsed.context.messages)
|
|
129
|
+
.map(messageUsageText)
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
|
|
132
|
+
if (shouldCountStablePromptOverhead(parsed)) {
|
|
133
|
+
if (parsed.context.systemPrompt?.length) parts.push(...parsed.context.systemPrompt);
|
|
134
|
+
if (parsed.context.tools?.length) parts.push(serializeForUsage(parsed.context.tools));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return estimateTokens(parts.join("\n"), parsed.modelId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function estimateKiroLogInputTokens(parsed: OcxParsedRequest): number {
|
|
141
|
+
const parts = parsed.context.messages.map(messageLogText).filter(Boolean);
|
|
142
|
+
if (parsed.context.systemPrompt?.length) parts.push(...parsed.context.systemPrompt);
|
|
143
|
+
if (parsed.context.tools?.length) parts.push(serializeForUsage(parsed.context.tools));
|
|
144
|
+
return Math.max(estimateKiroInputTokens(parsed), estimateTokens(parts.join("\n"), parsed.modelId));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function configuredKiroContextWindow(provider: OcxProviderConfig, modelId: string | undefined): number | undefined {
|
|
148
|
+
if (!modelId) return undefined;
|
|
149
|
+
const normalizedModelId = normalizeKiroModelId(modelId);
|
|
150
|
+
if (normalizedModelId === "auto") return undefined;
|
|
151
|
+
const window =
|
|
152
|
+
modelRecordValue(provider.modelContextWindows, modelId)
|
|
153
|
+
?? modelRecordValue(provider.modelContextWindows, normalizedModelId)
|
|
154
|
+
?? provider.contextWindow
|
|
155
|
+
?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
|
|
156
|
+
?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, normalizedModelId);
|
|
157
|
+
return typeof window === "number" && Number.isFinite(window) && window > 0 ? window : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function contextUsageTotalTokens(contextUsagePercentage: number | undefined, contextWindow: number | undefined): number | undefined {
|
|
161
|
+
if (contextUsagePercentage === undefined || contextUsagePercentage <= 0 || !contextWindow) return undefined;
|
|
162
|
+
return Math.max(0, Math.floor((contextUsagePercentage / 100) * contextWindow));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function kiroThinkingBudget(parsed: OcxParsedRequest): number | undefined {
|
|
166
|
+
const effort = parsed.options.reasoning;
|
|
167
|
+
if (!effort || effort === "none") return undefined;
|
|
168
|
+
const maxTokens = parsed.options.maxOutputTokens || 4096;
|
|
169
|
+
const percent: Record<string, number> = {
|
|
170
|
+
minimal: 0.10,
|
|
171
|
+
low: 0.20,
|
|
172
|
+
medium: 0.50,
|
|
173
|
+
high: 0.80,
|
|
174
|
+
xhigh: 0.95,
|
|
175
|
+
max: 0.95,
|
|
176
|
+
};
|
|
177
|
+
const ratio = percent[effort];
|
|
178
|
+
return ratio === undefined ? undefined : Math.max(1, Math.floor(maxTokens * ratio));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function injectKiroThinkingTags(content: string, parsed: OcxParsedRequest): string {
|
|
182
|
+
const budget = kiroThinkingBudget(parsed);
|
|
183
|
+
if (!budget) return content;
|
|
184
|
+
const instruction = [
|
|
185
|
+
"Think in English for better reasoning quality.",
|
|
186
|
+
"Be thorough and systematic, consider edge cases, challenge assumptions, and verify reasoning before answering.",
|
|
187
|
+
"After thinking, respond in the user's language.",
|
|
188
|
+
].join("\n");
|
|
189
|
+
return [
|
|
190
|
+
"<thinking_mode>enabled</thinking_mode>",
|
|
191
|
+
`<max_thinking_length>${budget}</max_thinking_length>`,
|
|
192
|
+
`<thinking_instruction>${instruction}</thinking_instruction>`,
|
|
193
|
+
"",
|
|
194
|
+
content,
|
|
195
|
+
].join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string | undefined): Record<string, unknown> {
|
|
199
|
+
const modelId = mapModelId(parsed.modelId);
|
|
200
|
+
const toolContext = convertKiroToolContext(parsed);
|
|
201
|
+
const kiroTools = toolContext.tools;
|
|
202
|
+
const systemParts: string[] = [];
|
|
203
|
+
if (!parsed.previousResponseId && parsed.context.systemPrompt?.length) systemParts.push(parsed.context.systemPrompt.join("\n\n"));
|
|
204
|
+
if (toolContext.systemAdditions.length > 0) systemParts.push(...toolContext.systemAdditions);
|
|
205
|
+
const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
|
|
206
|
+
const structuredToolIds = new Set<string>();
|
|
207
|
+
|
|
208
|
+
const mkUser = (content: string, images?: KiroImage[]): KiroHistoryEntry => ({
|
|
209
|
+
userInputMessage: {
|
|
210
|
+
content,
|
|
211
|
+
modelId,
|
|
212
|
+
origin: "AI_EDITOR",
|
|
213
|
+
...(images && images.length > 0 ? { images } : {}),
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const history: KiroHistoryEntry[] = [];
|
|
217
|
+
const fallbackEntries = new WeakSet<KiroHistoryEntry>();
|
|
218
|
+
let pending: KiroToolResult[] = [];
|
|
219
|
+
let lastRole = "";
|
|
220
|
+
const attachPending = (entry: KiroHistoryEntry): void => {
|
|
221
|
+
if (pending.length === 0) return;
|
|
222
|
+
const uim = entry.userInputMessage!;
|
|
223
|
+
uim.userInputMessageContext = { ...(uim.userInputMessageContext ?? {}), toolResults: pending };
|
|
224
|
+
pending = [];
|
|
225
|
+
};
|
|
226
|
+
const pushUserEntry = (entry: KiroHistoryEntry): void => {
|
|
227
|
+
if (pending.length === 0 && lastRole === "user") {
|
|
228
|
+
history.push({ assistantResponseMessage: { content: "(acknowledged)" } });
|
|
229
|
+
}
|
|
230
|
+
attachPending(entry);
|
|
231
|
+
history.push(entry);
|
|
232
|
+
lastRole = "user";
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
for (const msg of kiroPayloadMessages(parsed)) {
|
|
236
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
237
|
+
const text = userContentText((msg as { content: string | OcxContentPart[] }).content);
|
|
238
|
+
const images = extractKiroImages((msg as { content: string | OcxContentPart[] }).content);
|
|
239
|
+
pushUserEntry(mkUser(text, images));
|
|
240
|
+
} else if (msg.role === "assistant") {
|
|
241
|
+
if (pending.length > 0) {
|
|
242
|
+
const carrier = mkUser("(tool results)");
|
|
243
|
+
pushUserEntry(carrier);
|
|
244
|
+
}
|
|
245
|
+
const aMsg = msg as OcxAssistantMessage;
|
|
246
|
+
let text = (aMsg.content || [])
|
|
247
|
+
.filter((b): b is OcxTextContent => b.type === "text")
|
|
248
|
+
.map(b => b.text)
|
|
249
|
+
.join("");
|
|
250
|
+
const toolCalls = (aMsg.content || [])
|
|
251
|
+
.filter((b): b is OcxToolCall => b.type === "toolCall");
|
|
252
|
+
const toolUses: KiroToolUse[] = kiroTools.length > 0
|
|
253
|
+
? toolCalls.map(tc => {
|
|
254
|
+
const toolUseId = normalizeToolId(tc.id);
|
|
255
|
+
structuredToolIds.add(toolUseId);
|
|
256
|
+
return { name: tc.name, input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
|
|
257
|
+
})
|
|
258
|
+
: [];
|
|
259
|
+
if (kiroTools.length === 0) {
|
|
260
|
+
for (const toolCall of toolCalls) text = appendFallbackText(text, toolCallFallbackText(toolCall));
|
|
261
|
+
}
|
|
262
|
+
if (lastRole === "assistant") history.push(mkUser("(continue)"));
|
|
263
|
+
const entry: KiroHistoryEntry = { assistantResponseMessage: { content: text } };
|
|
264
|
+
if (toolUses.length > 0) entry.assistantResponseMessage!.toolUses = toolUses;
|
|
265
|
+
history.push(entry);
|
|
266
|
+
lastRole = "assistant";
|
|
267
|
+
} else if (msg.role === "toolResult") {
|
|
268
|
+
const tr = msg as OcxToolResultMessage;
|
|
269
|
+
const text = userContentText(tr.content);
|
|
270
|
+
const toolUseId = normalizeToolId(tr.toolCallId);
|
|
271
|
+
if (kiroTools.length > 0 && structuredToolIds.has(toolUseId)) {
|
|
272
|
+
pending.push({
|
|
273
|
+
content: [{ text: text || "(empty)" }],
|
|
274
|
+
status: tr.isError ? "error" : "success",
|
|
275
|
+
toolUseId,
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
if (pending.length > 0) pushUserEntry(mkUser("(tool results)"));
|
|
279
|
+
const fallback = mkUser(toolResultFallbackText(tr));
|
|
280
|
+
fallbackEntries.add(fallback);
|
|
281
|
+
pushUserEntry(fallback);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let currentEntry: KiroHistoryEntry;
|
|
287
|
+
if (pending.length > 0) {
|
|
288
|
+
currentEntry = mkUser("(tool results)");
|
|
289
|
+
attachPending(currentEntry);
|
|
290
|
+
} else if (history.length > 0 && history[history.length - 1].userInputMessage) {
|
|
291
|
+
currentEntry = history.pop()!;
|
|
292
|
+
} else {
|
|
293
|
+
currentEntry = mkUser("(continue)");
|
|
294
|
+
}
|
|
295
|
+
const currentUim = currentEntry.userInputMessage!;
|
|
296
|
+
|
|
297
|
+
if (systemPrefix) {
|
|
298
|
+
const firstUser = history.find(e => e.userInputMessage)?.userInputMessage;
|
|
299
|
+
if (firstUser) firstUser.content = systemPrefix + firstUser.content;
|
|
300
|
+
else currentUim.content = systemPrefix + currentUim.content;
|
|
301
|
+
}
|
|
302
|
+
if (kiroTools.length > 0) {
|
|
303
|
+
currentUim.userInputMessageContext = { ...(currentUim.userInputMessageContext ?? {}), tools: kiroTools };
|
|
304
|
+
}
|
|
305
|
+
if (!fallbackEntries.has(currentEntry) && !currentUim.userInputMessageContext?.toolResults && currentUim.content !== "(continue)") {
|
|
306
|
+
currentUim.content = injectKiroThinkingTags(currentUim.content, parsed);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const payload: Record<string, unknown> = {
|
|
310
|
+
conversationState: {
|
|
311
|
+
chatTriggerType: "MANUAL",
|
|
312
|
+
conversationId: stableConversationId(parsed),
|
|
313
|
+
currentMessage: { userInputMessage: currentUim },
|
|
314
|
+
...(history.length > 0 ? { history } : {}),
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
if (profileArn) payload.profileArn = profileArn;
|
|
318
|
+
return payload;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Stream parsing (shared by parseStream + parseResponse)
|
|
322
|
+
// CodeWhisperer GenerateAssistantResponse ALWAYS returns an AWS eventstream body (there is no
|
|
323
|
+
// non-streaming mode), so both the streaming bridge and the non-streaming web-search sidecar loop
|
|
324
|
+
// decode the same way — parseResponse just collects what parseStream yields.
|
|
325
|
+
export async function* parseKiroStream(
|
|
326
|
+
response: Response,
|
|
327
|
+
modelId?: string,
|
|
328
|
+
inputTokens = 0,
|
|
329
|
+
contextWindow?: number,
|
|
330
|
+
): AsyncGenerator<AdapterEvent> {
|
|
331
|
+
if (!response.body) {
|
|
332
|
+
yield { type: "error", message: "Kiro response has no body" };
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
let open: { id: string; name: string; chunks: string[] } | null = null;
|
|
336
|
+
// CW provides no usage; accumulate output chars and emit a heuristic estimate on done so Codex's
|
|
337
|
+
// usage display + auto-compact engage (see src/lib/token-estimate.ts).
|
|
338
|
+
let outputChars = "";
|
|
339
|
+
let contextUsagePercentage: number | undefined;
|
|
340
|
+
const thinking = new KiroThinkingParser();
|
|
341
|
+
const trackContent = (event: AdapterEvent): void => {
|
|
342
|
+
if ("text" in event) outputChars += event.text;
|
|
343
|
+
};
|
|
344
|
+
function* flushTool(): Generator<AdapterEvent> {
|
|
345
|
+
if (!open) return;
|
|
346
|
+
const tool = open;
|
|
347
|
+
open = null;
|
|
348
|
+
yield { type: "tool_call_start", id: tool.id, name: tool.name };
|
|
349
|
+
for (const chunk of tool.chunks) if (chunk) yield { type: "tool_call_delta", arguments: chunk };
|
|
350
|
+
yield { type: "tool_call_end" };
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
for await (const msg of decodeEventStream(response.body)) {
|
|
354
|
+
const mt = msg.headers[":message-type"];
|
|
355
|
+
if (mt === "exception" || mt === "error") {
|
|
356
|
+
// Terminal: surface the upstream error and never emit a trailing success-shaped `done`.
|
|
357
|
+
open = null;
|
|
358
|
+
yield { type: "error", message: safeKiroErrorMessage(msg.headers, new TextDecoder().decode(msg.payload)) };
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (mt && mt !== "event") continue;
|
|
362
|
+
const ev = parseKiroEvent(msg.payload);
|
|
363
|
+
if (!ev) continue;
|
|
364
|
+
switch (ev.type) {
|
|
365
|
+
case "usage":
|
|
366
|
+
break;
|
|
367
|
+
case "context_usage":
|
|
368
|
+
if (ev.contextUsagePercentage !== undefined && ev.contextUsagePercentage > 0) {
|
|
369
|
+
contextUsagePercentage = ev.contextUsagePercentage;
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
case "content":
|
|
373
|
+
if (open) {
|
|
374
|
+
open = null;
|
|
375
|
+
yield { type: "error", message: kiroTruncationErrorMessage("content arrived before tool stop") };
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (ev.data) {
|
|
379
|
+
for (const contentEvent of thinking.feed(ev.data)) {
|
|
380
|
+
trackContent(contentEvent);
|
|
381
|
+
yield contentEvent;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
case "tool_start": {
|
|
386
|
+
for (const contentEvent of thinking.flush()) {
|
|
387
|
+
trackContent(contentEvent);
|
|
388
|
+
yield contentEvent;
|
|
389
|
+
}
|
|
390
|
+
const id = ev.toolUseId || fallbackToolUseId();
|
|
391
|
+
const name = ev.name || "unknown";
|
|
392
|
+
if (open) {
|
|
393
|
+
if (open.id !== id || open.name !== name) {
|
|
394
|
+
open = null;
|
|
395
|
+
yield { type: "error", message: kiroTruncationErrorMessage("new tool started before previous tool stop") };
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
open = { id, name, chunks: [] };
|
|
400
|
+
}
|
|
401
|
+
yield { type: "heartbeat" };
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case "tool_input": {
|
|
405
|
+
for (const contentEvent of thinking.flush()) {
|
|
406
|
+
trackContent(contentEvent);
|
|
407
|
+
yield contentEvent;
|
|
408
|
+
}
|
|
409
|
+
if (!open) {
|
|
410
|
+
open = { id: ev.toolUseId || fallbackToolUseId(), name: ev.name || "unknown", chunks: [] };
|
|
411
|
+
}
|
|
412
|
+
if (open && ev.input) {
|
|
413
|
+
if (open.name === "unknown" && ev.name) open.name = ev.name;
|
|
414
|
+
open.chunks.push(ev.input);
|
|
415
|
+
outputChars += ev.input;
|
|
416
|
+
}
|
|
417
|
+
yield { type: "heartbeat" };
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "tool_stop": {
|
|
421
|
+
if (open) {
|
|
422
|
+
const input = open.chunks.join("");
|
|
423
|
+
if (!isCompleteKiroToolInput(input)) {
|
|
424
|
+
open = null;
|
|
425
|
+
yield { type: "error", message: kiroTruncationErrorMessage("incomplete tool input JSON") };
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
yield* flushTool();
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case "truncation":
|
|
433
|
+
open = null;
|
|
434
|
+
yield { type: "error", message: kiroTruncationErrorMessage(ev.data) };
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const contentEvent of thinking.flush()) {
|
|
439
|
+
trackContent(contentEvent);
|
|
440
|
+
yield contentEvent;
|
|
441
|
+
}
|
|
442
|
+
if (open) {
|
|
443
|
+
open = null;
|
|
444
|
+
yield { type: "error", message: kiroTruncationErrorMessage("stream ended before tool stop") };
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const outputTokens = estimateTokens(outputChars, modelId);
|
|
448
|
+
const usage: OcxUsage = { inputTokens, outputTokens, estimated: true };
|
|
449
|
+
const totalTokens = contextUsageTotalTokens(contextUsagePercentage, contextWindow);
|
|
450
|
+
if (totalTokens !== undefined) usage.totalTokens = totalTokens;
|
|
451
|
+
yield { type: "done", usage };
|
|
452
|
+
} catch (err) {
|
|
453
|
+
yield { type: "error", message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)) };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Adapter
|
|
458
|
+
export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
459
|
+
// Per-request closure (resolveAdapter builds a fresh adapter per request — server.ts:440 — so this
|
|
460
|
+
// is race-free) carrying the heuristic input-token estimate from buildRequest into the stream.
|
|
461
|
+
let inputTokens = 0;
|
|
462
|
+
let modelId: string | undefined;
|
|
463
|
+
let contextWindow: number | undefined;
|
|
464
|
+
return {
|
|
465
|
+
name: "kiro",
|
|
466
|
+
buildRequest(parsed: OcxParsedRequest) {
|
|
467
|
+
const region = resolveKiroApiRegion();
|
|
468
|
+
const profileArn = resolveKiroProfileArn();
|
|
469
|
+
const fp = fingerprint().slice(0, 64);
|
|
470
|
+
const headers: Record<string, string> = {
|
|
471
|
+
authorization: `Bearer ${provider.apiKey ?? ""}`,
|
|
472
|
+
"content-type": "application/x-amz-json-1.0",
|
|
473
|
+
accept: "application/vnd.amazon.eventstream",
|
|
474
|
+
"x-amz-target": AMZ_TARGET,
|
|
475
|
+
"user-agent": `aws-sdk-js/${SDK_VERSION} ua/2.1 os/${osTag()} lang/js md/nodejs#${NODE_VERSION} api/codewhispererstreaming#${SDK_VERSION} m/E KiroIDE-${KIRO_IDE_VERSION}-${fp}`,
|
|
476
|
+
"x-amz-user-agent": `aws-sdk-js/${SDK_VERSION} KiroIDE-${KIRO_IDE_VERSION}-${fp}`,
|
|
477
|
+
"x-amzn-codewhisperer-optout": "true",
|
|
478
|
+
"x-amzn-kiro-agent-mode": "vibe",
|
|
479
|
+
"amz-sdk-invocation-id": invocationId(),
|
|
480
|
+
};
|
|
481
|
+
if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
|
|
482
|
+
// CodeWhisperer GenerateAssistantResponse has no reasoning_effort field. Match kiro-gateway's
|
|
483
|
+
// fake-reasoning contract by injecting effort-derived thinking tags into only the current user turn.
|
|
484
|
+
const payload = buildKiroPayload(parsed, profileArn);
|
|
485
|
+
const body = JSON.stringify(payload);
|
|
486
|
+
debugProviderDiagnostic("kiro", "request", {
|
|
487
|
+
region,
|
|
488
|
+
requestedModel: parsed.modelId,
|
|
489
|
+
bodyBytes: new TextEncoder().encode(body).length,
|
|
490
|
+
messageCount: kiroPayloadMessages(parsed).length,
|
|
491
|
+
toolCount: parsed.context.tools?.length ?? 0,
|
|
492
|
+
hasProfileArn: Boolean(profileArn),
|
|
493
|
+
hasPreviousResponseId: Boolean(parsed.previousResponseId),
|
|
494
|
+
});
|
|
495
|
+
// CW returns no usage. Codex adds each response's usage into its session total; report only the
|
|
496
|
+
// current-turn input delta so old history is not repeatedly added to Codex's visible token usage.
|
|
497
|
+
modelId = parsed.modelId;
|
|
498
|
+
contextWindow = configuredKiroContextWindow(provider, parsed.modelId);
|
|
499
|
+
inputTokens = estimateKiroInputTokens(parsed);
|
|
500
|
+
return {
|
|
501
|
+
url: `https://runtime.${region}.kiro.dev/`,
|
|
502
|
+
method: "POST",
|
|
503
|
+
headers,
|
|
504
|
+
body,
|
|
505
|
+
usageLog: { inputTokens: estimateKiroLogInputTokens(parsed), estimated: true },
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
parseStream(response: Response): AsyncGenerator<AdapterEvent> {
|
|
510
|
+
return parseKiroStream(response, modelId, inputTokens, contextWindow);
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> {
|
|
514
|
+
return fetchKiroWithRetry(request, ctx);
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
// Non-streaming path used by the web-search sidecar loop (loop.ts runs each iteration
|
|
518
|
+
// non-streamed so it can inspect tool calls). CW only ever event-streams, so we drain the
|
|
519
|
+
// same decoder into an array. Without this, any Codex request that includes the web_search
|
|
520
|
+
// tool failed with "web-search sidecar requires a non-streaming adapter" (kiro-only).
|
|
521
|
+
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
522
|
+
const events: AdapterEvent[] = [];
|
|
523
|
+
for await (const e of parseKiroStream(response, modelId, inputTokens, contextWindow)) events.push(e);
|
|
524
|
+
return events;
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
@@ -5,6 +5,15 @@ import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoice
|
|
|
5
5
|
import { mapReasoningEffort } from "../reasoning-effort";
|
|
6
6
|
import { contentPartsToText } from "./image";
|
|
7
7
|
|
|
8
|
+
// Z.AI's "glm-5.2[1m]" 1M-context id is a Claude-Code / Anthropic-endpoint-only
|
|
9
|
+
// convention; OpenAI-compatible chat-completions endpoints reject the bracketed
|
|
10
|
+
// suffix (Z.AI 400 code 1211 "Unknown Model"). Strip a single trailing "[...]"
|
|
11
|
+
// group from the wire model id so the bare id is sent. Applies to the
|
|
12
|
+
// openai-chat path only — the anthropic adapter keeps the suffix verbatim.
|
|
13
|
+
export function stripBracketedModelSuffix(modelId: string): string {
|
|
14
|
+
return modelId.replace(/\[[^\]]*\]\s*$/, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
9
18
|
const out: unknown[] = [];
|
|
10
19
|
const { context, options } = parsed;
|
|
@@ -163,7 +172,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
163
172
|
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools);
|
|
164
173
|
|
|
165
174
|
const body: Record<string, unknown> = {
|
|
166
|
-
model: parsed.modelId,
|
|
175
|
+
model: stripBracketedModelSuffix(parsed.modelId),
|
|
167
176
|
messages,
|
|
168
177
|
stream: parsed.stream,
|
|
169
178
|
};
|
package/src/bridge.ts
CHANGED
|
@@ -14,7 +14,7 @@ function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
|
|
|
14
14
|
const out: Record<string, unknown> = {
|
|
15
15
|
input_tokens: usage.inputTokens,
|
|
16
16
|
output_tokens: usage.outputTokens,
|
|
17
|
-
total_tokens: usage.inputTokens + usage.outputTokens,
|
|
17
|
+
total_tokens: usage.totalTokens ?? usage.inputTokens + usage.outputTokens,
|
|
18
18
|
};
|
|
19
19
|
if (usage.cachedInputTokens !== undefined) {
|
|
20
20
|
out.input_tokens_details = { cached_tokens: usage.cachedInputTokens };
|
package/src/cli.ts
CHANGED
|
@@ -155,17 +155,39 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
155
155
|
if (!process.env.OCX_SERVICE) { try { restoreNativeCodex(); } catch { /* best-effort restore */ } }
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
+
let shuttingDown = false;
|
|
159
|
+
let shutdownStartedAt = 0;
|
|
160
|
+
// Terminal Ctrl-C delivers SIGINT to the whole foreground group AND the launcher
|
|
161
|
+
// forwards its own — two signals land within milliseconds. Treat a duplicate inside
|
|
162
|
+
// this window as the same Ctrl-C (one graceful drain); a deliberate later press
|
|
163
|
+
// escalates to an immediate force-exit ("gradual kill").
|
|
164
|
+
const FORCE_AFTER_MS = 500;
|
|
158
165
|
const shutdown = () => {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
if (shuttingDown) {
|
|
168
|
+
if (now - shutdownStartedAt < FORCE_AFTER_MS) return; // near-simultaneous duplicate — ignore
|
|
169
|
+
console.log("\n⏹ Force shutdown (second signal).");
|
|
170
|
+
try { syncCleanup(); } catch { /* best-effort */ }
|
|
171
|
+
process.exit(130);
|
|
172
|
+
}
|
|
173
|
+
shuttingDown = true;
|
|
174
|
+
shutdownStartedAt = now;
|
|
159
175
|
console.log("\n🛑 Shutting down opencodex proxy...");
|
|
160
176
|
void (async () => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
try {
|
|
178
|
+
await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
|
|
179
|
+
} finally {
|
|
180
|
+
syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
164
183
|
})();
|
|
165
184
|
};
|
|
166
185
|
|
|
167
186
|
process.on("SIGINT", shutdown);
|
|
168
187
|
process.on("SIGTERM", shutdown);
|
|
188
|
+
// The launcher (bin/ocx.mjs) forwards SIGHUP too (e.g. terminal close); handle it
|
|
189
|
+
// gracefully here so it drains + cleans up instead of a default immediate kill.
|
|
190
|
+
process.on("SIGHUP", shutdown);
|
|
169
191
|
process.on("exit", syncCleanup);
|
|
170
192
|
|
|
171
193
|
await maybeShowStarPrompt(); // once-only [Y/n] GitHub-star prompt on first interactive start
|