| import { BaseExecutor } from "./base.js"; |
| import { PROVIDERS } from "../config/providers.js"; |
| import { v4 as uuidv4 } from "uuid"; |
| import { refreshKiroToken } from "../services/tokenRefresh.js"; |
|
|
| |
| |
| |
| |
| export class KiroExecutor extends BaseExecutor { |
| constructor() { |
| super("kiro", PROVIDERS.kiro); |
| } |
|
|
| buildHeaders(credentials, stream = true) { |
| const headers = { |
| ...this.config.headers, |
| "Amz-Sdk-Request": "attempt=1; max=3", |
| "Amz-Sdk-Invocation-Id": uuidv4() |
| }; |
|
|
| if (credentials.accessToken) { |
| headers["Authorization"] = `Bearer ${credentials.accessToken}`; |
| } |
|
|
| return headers; |
| } |
|
|
| transformRequest(model, body, stream, credentials) { |
| return body; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async execute(args) { |
| const result = await super.execute(args); |
| if (result?.response?.ok) { |
| result.response = this.transformEventStreamToSSE(result.response, args.model); |
| } |
| return result; |
| } |
|
|
| |
| |
| |
| |
| transformEventStreamToSSE(response, model) { |
| let buffer = new Uint8Array(0); |
| let chunkIndex = 0; |
| const responseId = `chatcmpl-${Date.now()}`; |
| const created = Math.floor(Date.now() / 1000); |
| const state = { |
| endDetected: false, |
| finishEmitted: false, |
| hasToolCalls: false, |
| hasReasoningContent: false, |
| reasoningChunkCount: 0, |
| toolCallIndex: 0, |
| seenToolIds: new Map() |
| }; |
|
|
| const transformStream = new TransformStream({ |
| async transform(chunk, controller) { |
| |
| const newBuffer = new Uint8Array(buffer.length + chunk.length); |
| newBuffer.set(buffer); |
| newBuffer.set(chunk, buffer.length); |
| buffer = newBuffer; |
|
|
| |
| let iterations = 0; |
| const maxIterations = 1000; |
| while (buffer.length >= 16 && iterations < maxIterations) { |
| iterations++; |
| const view = new DataView(buffer.buffer, buffer.byteOffset); |
| const totalLength = view.getUint32(0, false); |
|
|
| if (totalLength < 16 || totalLength > buffer.length || buffer.length < totalLength) break; |
|
|
| const eventData = buffer.slice(0, totalLength); |
| buffer = buffer.slice(totalLength); |
|
|
| const event = parseEventFrame(eventData); |
| if (!event) continue; |
|
|
| const eventType = event.headers[":event-type"] || ""; |
| |
| |
| if (!state.totalContentLength) state.totalContentLength = 0; |
| if (!state.contextUsagePercentage) state.contextUsagePercentage = 0; |
|
|
| |
| if (eventType === "assistantResponseEvent" && event.payload?.content) { |
| const content = event.payload.content; |
| state.totalContentLength += content.length; |
| |
| const chunk = { |
| id: responseId, |
| object: "chat.completion.chunk", |
| created, |
| model, |
| choices: [{ |
| index: 0, |
| delta: chunkIndex === 0 |
| ? { role: "assistant", content } |
| : { content }, |
| finish_reason: null |
| }] |
| }; |
| chunkIndex++; |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); |
| } |
|
|
| |
| |
| |
| |
| |
| if (eventType === "reasoningContentEvent") { |
| const reasoning = event.payload?.reasoningContentEvent || event.payload || {}; |
| const reasoningText = (typeof reasoning === "string") |
| ? reasoning |
| : (reasoning.text || reasoning.content || ""); |
| if (reasoningText) { |
| state.hasReasoningContent = true; |
| state.totalContentLength += reasoningText.length; |
|
|
| const reasoningDelta = state.reasoningChunkCount === 0 && chunkIndex === 0 |
| ? { role: "assistant", reasoning_content: reasoningText } |
| : { reasoning_content: reasoningText }; |
|
|
| const chunk = { |
| id: responseId, |
| object: "chat.completion.chunk", |
| created, |
| model, |
| choices: [{ |
| index: 0, |
| delta: reasoningDelta, |
| finish_reason: null |
| }] |
| }; |
| chunkIndex++; |
| state.reasoningChunkCount++; |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); |
| } |
| } |
|
|
| |
| if (eventType === "codeEvent" && event.payload?.content) { |
| const chunk = { |
| id: responseId, |
| object: "chat.completion.chunk", |
| created, |
| model, |
| choices: [{ |
| index: 0, |
| delta: { content: event.payload.content }, |
| finish_reason: null |
| }] |
| }; |
| chunkIndex++; |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); |
| } |
|
|
| |
| if (eventType === "toolUseEvent" && event.payload) { |
| state.hasToolCalls = true; |
| const toolUse = event.payload; |
| const toolUses = Array.isArray(toolUse) ? toolUse : [toolUse]; |
|
|
| for (const singleToolUse of toolUses) { |
| const toolCallId = singleToolUse.toolUseId || `call_${Date.now()}`; |
| const toolName = singleToolUse.name || ""; |
| const toolInput = singleToolUse.input; |
|
|
| let toolIndex; |
| const isNewTool = !state.seenToolIds.has(toolCallId); |
|
|
| if (isNewTool) { |
| toolIndex = state.toolCallIndex++; |
| state.seenToolIds.set(toolCallId, toolIndex); |
|
|
| const startChunk = { |
| id: responseId, |
| object: "chat.completion.chunk", |
| created, |
| model, |
| choices: [{ |
| index: 0, |
| delta: { |
| ...(chunkIndex === 0 ? { role: "assistant" } : {}), |
| tool_calls: [{ |
| index: toolIndex, |
| id: toolCallId, |
| type: "function", |
| function: { |
| name: toolName, |
| arguments: "" |
| } |
| }] |
| }, |
| finish_reason: null |
| }] |
| }; |
| chunkIndex++; |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(startChunk)}\n\n`)); |
| } else { |
| toolIndex = state.seenToolIds.get(toolCallId); |
| } |
|
|
| if (toolInput !== undefined) { |
| let argumentsStr; |
|
|
| if (typeof toolInput === 'string') { |
| argumentsStr = toolInput; |
| } else if (typeof toolInput === 'object') { |
| argumentsStr = JSON.stringify(toolInput); |
| } else { |
| continue; |
| } |
|
|
| const argsChunk = { |
| id: responseId, |
| object: "chat.completion.chunk", |
| created, |
| model, |
| choices: [{ |
| index: 0, |
| delta: { |
| tool_calls: [{ |
| index: toolIndex, |
| function: { |
| arguments: argumentsStr |
| } |
| }] |
| }, |
| finish_reason: null |
| }] |
| }; |
| chunkIndex++; |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(argsChunk)}\n\n`)); |
| } |
| } |
| } |
|
|
| |
| if (eventType === "messageStopEvent") { |
| const chunk = { |
| id: responseId, |
| object: "chat.completion.chunk", |
| created, |
| model, |
| choices: [{ |
| index: 0, |
| delta: {}, |
| finish_reason: state.hasToolCalls ? "tool_calls" : "stop" |
| }] |
| }; |
| state.finishEmitted = true; |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`)); |
| } |
|
|
| |
| if (eventType === "contextUsageEvent" && event.payload?.contextUsagePercentage) { |
| state.contextUsagePercentage = event.payload.contextUsagePercentage; |
| |
| state.hasContextUsage = true; |
| } |
|
|
| |
| if (eventType === "meteringEvent") { |
| state.hasMeteringEvent = true; |
| } |
|
|
| |
| if (eventType === "metricsEvent") { |
| |
| const metrics = event.payload?.metricsEvent || event.payload; |
| if (metrics && typeof metrics === 'object') { |
| const inputTokens = metrics.inputTokens || 0; |
| const outputTokens = metrics.outputTokens || 0; |
| |
| if (inputTokens > 0 || outputTokens > 0) { |
| state.usage = { |
| prompt_tokens: inputTokens, |
| completion_tokens: outputTokens, |
| total_tokens: inputTokens + outputTokens |
| }; |
| } |
| } |
| } |
|
|
| |
| if (state.hasMeteringEvent && state.hasContextUsage && !state.finishEmitted) { |
| state.finishEmitted = true; |
| |
| |
| if (!state.usage) { |
| |
| const estimatedOutputTokens = state.totalContentLength > 0 |
| ? Math.max(1, Math.floor(state.totalContentLength / 4)) |
| : 0; |
| |
| |
| |
| const estimatedInputTokens = state.contextUsagePercentage > 0 |
| ? Math.floor(state.contextUsagePercentage * 200000 / 100) |
| : 0; |
| |
| state.usage = { |
| prompt_tokens: estimatedInputTokens, |
| completion_tokens: estimatedOutputTokens, |
| total_tokens: estimatedInputTokens + estimatedOutputTokens |
| }; |
| } |
| |
| const finishChunk = { |
| id: responseId, |
| object: "chat.completion.chunk", |
| created, |
| model, |
| choices: [{ |
| index: 0, |
| delta: {}, |
| finish_reason: state.hasToolCalls ? "tool_calls" : "stop" |
| }] |
| }; |
| |
| |
| if (state.usage) { |
| finishChunk.usage = state.usage; |
| } |
| |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(finishChunk)}\n\n`)); |
| } |
| } |
|
|
| if (iterations >= maxIterations) { |
| console.warn("[Kiro] Max iterations reached in event parsing"); |
| } |
| }, |
|
|
| flush(controller) { |
| |
| if (!state.finishEmitted) { |
| state.finishEmitted = true; |
| const finishChunk = { |
| id: responseId, |
| object: "chat.completion.chunk", |
| created, |
| model, |
| choices: [{ |
| index: 0, |
| delta: {}, |
| finish_reason: state.hasToolCalls ? "tool_calls" : "stop" |
| }] |
| }; |
| controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(finishChunk)}\n\n`)); |
| } |
|
|
| |
| controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")); |
| } |
| }); |
|
|
| |
| if (!response.body) { |
| return new Response("data: [DONE]\n\n", { status: response.status, headers: { "Content-Type": "text/event-stream" } }); |
| } |
| const transformedStream = response.body.pipeThrough(transformStream); |
|
|
| return new Response(transformedStream, { |
| status: response.status, |
| statusText: response.statusText, |
| headers: { |
| "Content-Type": "text/event-stream", |
| "Cache-Control": "no-cache", |
| "Connection": "keep-alive" |
| } |
| }); |
| } |
|
|
| async refreshCredentials(credentials, log, proxyOptions = null) { |
| if (!credentials.refreshToken) return null; |
|
|
| try { |
| |
| const result = await refreshKiroToken( |
| credentials.refreshToken, |
| credentials.providerSpecificData, |
| log, |
| proxyOptions |
| ); |
|
|
| return result; |
| } catch (error) { |
| log?.error?.("TOKEN", `Kiro refresh error: ${error.message}`); |
| return null; |
| } |
| } |
| } |
|
|
| |
| |
| |
| function parseEventFrame(data) { |
| try { |
| const view = new DataView(data.buffer, data.byteOffset); |
| const headersLength = view.getUint32(4, false); |
|
|
| |
| const headers = {}; |
| let offset = 12; |
| const headerEnd = 12 + headersLength; |
|
|
| while (offset < headerEnd && offset < data.length) { |
| const nameLen = data[offset]; |
| offset++; |
| if (offset + nameLen > data.length) break; |
|
|
| const name = new TextDecoder().decode(data.slice(offset, offset + nameLen)); |
| offset += nameLen; |
|
|
| const headerType = data[offset]; |
| offset++; |
|
|
| if (headerType === 7) { |
| const valueLen = (data[offset] << 8) | data[offset + 1]; |
| offset += 2; |
| if (offset + valueLen > data.length) break; |
|
|
| const value = new TextDecoder().decode(data.slice(offset, offset + valueLen)); |
| offset += valueLen; |
| headers[name] = value; |
| } else { |
| break; |
| } |
| } |
|
|
| |
| const payloadStart = 12 + headersLength; |
| const payloadEnd = data.length - 4; |
|
|
| let payload = null; |
| if (payloadEnd > payloadStart) { |
| const payloadStr = new TextDecoder().decode(data.slice(payloadStart, payloadEnd)); |
|
|
| |
| if (!payloadStr || !payloadStr.trim()) { |
| return { headers, payload: null }; |
| } |
|
|
| try { |
| payload = JSON.parse(payloadStr); |
| } catch (parseError) { |
| |
| console.warn(`[Kiro] Failed to parse payload: ${parseError.message} | payload: ${payloadStr.substring(0, 100)}`); |
| payload = { raw: payloadStr }; |
| } |
| } |
|
|
| return { headers, payload }; |
| } catch { |
| return null; |
| } |
| } |
|
|
| export default KiroExecutor; |
|
|