Merge origin/master (v0.5.81) into gitea/new_feature
Resolve conflicts: - streamingHandler.js: merge buildStreamErrorBytes onAbortTerminal + local shouldPersistRequestDetail & streamStatusForContent - capabilities.js: preserve server-injected user-asserted caps and models.dev catalog lookup; wire CommandCode /alpha/generate caps inside resolve() - commandcode.js (services/usage): adopt upstream whoami + billing credits/subscriptions with 5h/weekly rate windows and plan caps - openai-to-commandcode.js: merge toNativeImageBlock (data-URI & http(s) support) and assistant reasoning_content preservation - commandcode-to-openai.js: adopt upstream mid-stream error throw for clean retry and abortion - tests: sync commandcode test suite and exclude .next from vitest config
This commit is contained in:
@@ -34,6 +34,7 @@ import { ROLE, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import {
|
||||
canonicalizeKiroConversation,
|
||||
normalizeKiroToolSpecs,
|
||||
kiroEmptyUserContent,
|
||||
} from "../concerns/kiroConversation.js";
|
||||
|
||||
/**
|
||||
@@ -53,7 +54,8 @@ function convertClaudeMessagesToKiro(messages, model) {
|
||||
|
||||
const flushPending = () => {
|
||||
if (currentRole === ROLE.USER) {
|
||||
const content = pendingUserContent.join("\n\n").trim() || "continue";
|
||||
const content = pendingUserContent.join("\n\n").trim()
|
||||
|| kiroEmptyUserContent(pendingToolResults.length > 0);
|
||||
const userMsg = { userInputMessage: { content, modelId: model } };
|
||||
|
||||
if (pendingImages.length > 0) {
|
||||
@@ -97,11 +99,21 @@ function convertClaudeMessagesToKiro(messages, model) {
|
||||
if (typeof block.content === "string") {
|
||||
resultContent = block.content;
|
||||
} else if (Array.isArray(block.content)) {
|
||||
// Images a tool returned (screenshots) ride along as user images;
|
||||
// Kiro tool results are text-only.
|
||||
let hasImage = false;
|
||||
for (const c of block.content) {
|
||||
if (c?.type === CLAUDE_BLOCK.IMAGE && c.source?.type === "base64") {
|
||||
hasImage = true;
|
||||
const imageType = c.source.media_type || DEFAULT_IMAGE_MIME;
|
||||
pendingImages.push({ format: imageType.split("/")[1] || imageType, source: { bytes: c.source.data } });
|
||||
}
|
||||
}
|
||||
resultContent =
|
||||
block.content
|
||||
.filter((c) => c.type === CLAUDE_BLOCK.TEXT)
|
||||
.map((c) => c.text)
|
||||
.join("\n") || JSON.stringify(block.content);
|
||||
.join("\n") || (hasImage ? "(image attached)" : JSON.stringify(block.content));
|
||||
} else if (block.content) {
|
||||
resultContent = JSON.stringify(block.content);
|
||||
}
|
||||
@@ -341,6 +353,13 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
// Kiro tool specs get sanitized names (`mcp__a__b` → `mcp_a_b`); keep the
|
||||
// reverse map so tool calls stream back under the client's own names.
|
||||
const restoredToolNames = new Map();
|
||||
for (const [original, sanitized] of nameMap) {
|
||||
if (original !== sanitized) restoredToolNames.set(sanitized, original);
|
||||
}
|
||||
if (restoredToolNames.size) payload._toolNameMap = restoredToolNames;
|
||||
return payload;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,25 +196,41 @@ function convertClaudeMessage(msg) {
|
||||
});
|
||||
break;
|
||||
|
||||
case CLAUDE_BLOCK.TOOL_RESULT:
|
||||
case CLAUDE_BLOCK.TOOL_RESULT: {
|
||||
let resultContent = "";
|
||||
const resultImages = [];
|
||||
if (typeof block.content === "string") {
|
||||
resultContent = block.content;
|
||||
} else if (Array.isArray(block.content)) {
|
||||
resultContent = block.content
|
||||
.filter(c => c.type === CLAUDE_BLOCK.TEXT)
|
||||
.map(c => c.text)
|
||||
.join("\n") || JSON.stringify(block.content);
|
||||
for (const c of block.content) {
|
||||
if (c?.type === CLAUDE_BLOCK.IMAGE && c.source?.type === "base64") {
|
||||
resultImages.push({
|
||||
type: OPENAI_BLOCK.IMAGE_URL,
|
||||
image_url: { url: encodeDataUri(c.source.media_type, c.source.data) }
|
||||
});
|
||||
}
|
||||
}
|
||||
const textOnly = block.content.filter(c => c?.type === CLAUDE_BLOCK.TEXT);
|
||||
resultContent = textOnly.map(c => c.text).join("\n")
|
||||
|| (resultImages.length ? "" : JSON.stringify(block.content));
|
||||
} else if (block.content) {
|
||||
resultContent = JSON.stringify(block.content);
|
||||
}
|
||||
|
||||
|
||||
toolResults.push({
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: block.tool_use_id,
|
||||
content: resultContent
|
||||
});
|
||||
// The OpenAI tool role is text-only, so a screenshot or any other image a
|
||||
// tool returned would otherwise vanish. Hand it to the model in the user
|
||||
// turn that follows the tool messages, tagged with the call it came from.
|
||||
if (resultImages.length) {
|
||||
parts.push({ type: OPENAI_BLOCK.TEXT, text: `[Image from tool result ${block.tool_use_id}]` });
|
||||
parts.push(...resultImages);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* - params.system: STRING at top level (Anthropic-style; system messages NOT allowed in messages[])
|
||||
* - params.messages[*].role ∈ {"user","assistant","tool"}
|
||||
* - params.messages[*].content: Array of content blocks (NEVER a string)
|
||||
* - image_url / image source → {type:"image", image:"data:...;base64,...", mimeType}
|
||||
* - tool_use blocks (assistant): {type:"tool-call", toolCallId, toolName, input}
|
||||
* - tool_result blocks (role=user): {type:"tool-result", toolCallId, toolName, output}
|
||||
* - tools[*]: Anthropic plain {name, description, input_schema}
|
||||
@@ -12,10 +13,9 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { randomUUID } from "crypto";
|
||||
import { ROLE, OPENAI_BLOCK } from "../schema/index.js";
|
||||
import { DEFAULT_IMAGE_MIME } from "../schema/index.js";
|
||||
import { parseDataUri } from "../concerns/image.js";
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js";
|
||||
import { parseDataUri, encodeDataUri } from "../concerns/image.js";
|
||||
|
||||
function flattenText(content) {
|
||||
if (content == null) return "";
|
||||
@@ -32,6 +32,58 @@ function flattenText(content) {
|
||||
return String(content);
|
||||
}
|
||||
|
||||
function toNativeImageBlock(part) {
|
||||
if (!part || typeof part !== "object") return null;
|
||||
|
||||
if (part.type === OPENAI_BLOCK.IMAGE_URL) {
|
||||
const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
|
||||
if (!url) return null;
|
||||
const parsed = parseDataUri(url);
|
||||
if (parsed) {
|
||||
return {
|
||||
type: OPENAI_BLOCK.IMAGE,
|
||||
image: encodeDataUri(parsed.mimeType, parsed.base64),
|
||||
mimeType: parsed.mimeType,
|
||||
};
|
||||
}
|
||||
if (typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"))) {
|
||||
return {
|
||||
type: OPENAI_BLOCK.IMAGE,
|
||||
image: url,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (part.type === OPENAI_BLOCK.IMAGE || part.type === CLAUDE_BLOCK.IMAGE) {
|
||||
if (typeof part.image === "string" && part.image.startsWith("data:")) {
|
||||
const parsed = parseDataUri(part.image);
|
||||
return {
|
||||
type: OPENAI_BLOCK.IMAGE,
|
||||
image: part.image,
|
||||
mimeType: part.mimeType || parsed?.mimeType || "image/png",
|
||||
};
|
||||
}
|
||||
if (typeof part.image === "string" && (part.image.startsWith("http://") || part.image.startsWith("https://"))) {
|
||||
return {
|
||||
type: OPENAI_BLOCK.IMAGE,
|
||||
image: part.image,
|
||||
};
|
||||
}
|
||||
const source = part.source;
|
||||
if (source?.type === "base64" && typeof source.data === "string") {
|
||||
const mime = source.media_type || "image/png";
|
||||
return {
|
||||
type: OPENAI_BLOCK.IMAGE,
|
||||
image: encodeDataUri(mime, source.data),
|
||||
mimeType: mime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function toContentBlocks(content) {
|
||||
if (content == null) return [{ type: OPENAI_BLOCK.TEXT, text: "" }];
|
||||
if (typeof content === "string")
|
||||
@@ -44,30 +96,12 @@ function toContentBlocks(content) {
|
||||
} else if (part && typeof part === "object") {
|
||||
if (part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string") {
|
||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
} else if (
|
||||
part.type === OPENAI_BLOCK.IMAGE_URL ||
|
||||
part.type === OPENAI_BLOCK.IMAGE
|
||||
) {
|
||||
// CommandCode `/alpha/generate` accepts {type:"image", image:"<data URI | url>"} —
|
||||
// same shape the official command-code CLI sends (verified from CLI source).
|
||||
const src = part.source;
|
||||
let raw = part.image_url?.url || src?.data || src?.url || "";
|
||||
let parsed = parseDataUri(raw);
|
||||
if (!parsed && src?.type === "base64" && src?.data) {
|
||||
// Claude-style base64 source without a data-URI prefix → wrap it.
|
||||
raw = `data:${src.media_type || DEFAULT_IMAGE_MIME};base64,${src.data}`;
|
||||
parsed = parseDataUri(raw);
|
||||
} else {
|
||||
const image = toNativeImageBlock(part);
|
||||
if (image) blocks.push(image);
|
||||
else if (typeof part.text === "string") {
|
||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
}
|
||||
if (parsed) {
|
||||
blocks.push({
|
||||
type: "image",
|
||||
image: `data:${parsed.mimeType};base64,${parsed.base64}`,
|
||||
});
|
||||
} else if (raw) {
|
||||
blocks.push({ type: "image", image: raw });
|
||||
}
|
||||
} else if (typeof part.text === "string") {
|
||||
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +153,10 @@ function convertMessages(messages = []) {
|
||||
|
||||
if (role === ROLE.ASSISTANT) {
|
||||
const blocks = [];
|
||||
const rc = m.reasoning_content || m.thought || m.reasoning;
|
||||
if (rc || (Array.isArray(m.tool_calls) && m.tool_calls.length > 0)) {
|
||||
blocks.push({ type: "reasoning", text: rc || " " });
|
||||
}
|
||||
const text = flattenText(m.content);
|
||||
if (text) blocks.push({ type: OPENAI_BLOCK.TEXT, text });
|
||||
if (Array.isArray(m.tool_calls)) {
|
||||
|
||||
@@ -129,7 +129,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
|
||||
if (tc.type !== OPENAI_BLOCK.FUNCTION) continue;
|
||||
|
||||
const args = tryParseJSON(tc.function?.arguments || "{}");
|
||||
const cachedSig = tc.id ? getGeminiThoughtSignatureSync(tc.id, sessionId) : null;
|
||||
const cachedSig = tc.id ? getGeminiThoughtSignatureSync(tc.id, sessionId, model) : null;
|
||||
// First call gets cached signature or fallback; sibling calls remain unsigned if no cached sig
|
||||
const callSig = cachedSig || (!firstFunctionCallSeen ? signature : undefined);
|
||||
firstFunctionCallSeen = true;
|
||||
@@ -341,7 +341,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
|
||||
if (block.type === CLAUDE_BLOCK.TEXT) {
|
||||
parts.push({ text: block.text });
|
||||
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
const cachedSig = block.id ? getGeminiThoughtSignatureSync(block.id, credentials?._clientSessionId) : null;
|
||||
const cachedSig = block.id ? getGeminiThoughtSignatureSync(block.id, credentials?._clientSessionId, model) : null;
|
||||
const callSig = cachedSig || (!firstToolUseSeen ? signature : undefined);
|
||||
firstToolUseSeen = true;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import {
|
||||
canonicalizeKiroConversation,
|
||||
normalizeKiroToolSpecs,
|
||||
kiroEmptyUserContent,
|
||||
} from "../concerns/kiroConversation.js";
|
||||
|
||||
/**
|
||||
@@ -51,7 +52,8 @@ function convertMessages(messages, model) {
|
||||
|
||||
const flushPending = () => {
|
||||
if (currentRole === "user") {
|
||||
const content = pendingUserContent.join("\n\n").trim() || "continue";
|
||||
const content = pendingUserContent.join("\n\n").trim()
|
||||
|| kiroEmptyUserContent(pendingToolResults.length > 0);
|
||||
const userMsg = {
|
||||
userInputMessage: {
|
||||
content: content,
|
||||
@@ -434,6 +436,13 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
// Kiro tool specs get sanitized names (`mcp__a__b` → `mcp_a_b`); keep the
|
||||
// reverse map so tool calls stream back under the client's own names.
|
||||
const restoredToolNames = new Map();
|
||||
for (const [original, sanitized] of nameMap) {
|
||||
if (original !== sanitized) restoredToolNames.set(sanitized, original);
|
||||
}
|
||||
if (restoredToolNames.size) payload._toolNameMap = restoredToolNames;
|
||||
return payload;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user