fix(kiro): deliver system prompt natively, add Opus 4.5/4.7/4.8, tolerate dash version ids (#2366)
- Pass system prompt via native systemInstruction field (+ <instructions> fallback) so Claude models stop treating it as info-only <system-reminder> - Add Opus 4.5/4.7/4.8 (base/thinking/agentic/thinking+agentic) to Kiro registry - normalizeModelId(): dash->dot version separator, scoped to Kiro provider only - Replace <system-reminder> with <instructions> in claude-to-openai/openai-to-kiro Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { PROVIDERS } from "./providers.js";
|
||||
import REGISTRY from "../providers/registry/index.js";
|
||||
// PROVIDER_MODELS now built from providers/registry (transport + models co-located)
|
||||
import { PROVIDER_MODELS } from "../providers/index.js";
|
||||
import { modelQuotaFamily, modelStrip, modelTargetFormat } from "../providers/models/schema.js";
|
||||
import { modelQuotaFamily, modelStrip, modelTargetFormat, normalizeModelId } from "../providers/models/schema.js";
|
||||
import { CODEX_REVIEW_SUFFIX } from "../providers/models/helpers.js";
|
||||
|
||||
export { PROVIDER_MODELS };
|
||||
@@ -18,37 +18,55 @@ export function getDefaultModel(aliasOrId) {
|
||||
return models?.[0]?.id || null;
|
||||
}
|
||||
|
||||
// Providers whose registry uses dots in version numbers (e.g. "claude-sonnet-4.5").
|
||||
// For these, we tolerate clients sending dashes ("claude-sonnet-4-5") by normalizing
|
||||
// digit-hyphen-digit to digit-dot-digit before lookup. Other providers are left untouched.
|
||||
const DOT_VERSION_PROVIDERS = new Set(["kr", "kiro"]);
|
||||
|
||||
// Find a registry entry by id. For Kiro models, tolerates dash/dot version separators
|
||||
// ("claude-sonnet-4-5" ~= "claude-sonnet-4.5"). Other providers use exact match only.
|
||||
function findModel(models, modelId, aliasOrId) {
|
||||
if (!models) return undefined;
|
||||
const found = models.find(m => m.id === modelId);
|
||||
if (found) return found;
|
||||
if (!DOT_VERSION_PROVIDERS.has(aliasOrId)) return undefined;
|
||||
const normalized = normalizeModelId(modelId);
|
||||
if (normalized === modelId) return undefined;
|
||||
return models.find(m => m.id === normalized);
|
||||
}
|
||||
|
||||
export function isValidModel(aliasOrId, modelId, passthroughProviders = new Set()) {
|
||||
if (passthroughProviders.has(aliasOrId)) return true;
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return false;
|
||||
return models.some(m => m.id === modelId);
|
||||
return !!findModel(models, modelId, aliasOrId);
|
||||
}
|
||||
|
||||
export function findModelName(aliasOrId, modelId) {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return modelId;
|
||||
const found = models.find(m => m.id === modelId);
|
||||
const found = findModel(models, modelId, aliasOrId);
|
||||
return found?.name || modelId;
|
||||
}
|
||||
|
||||
export function getModelTargetFormat(aliasOrId, modelId) {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return null;
|
||||
return modelTargetFormat(models.find(m => m.id === modelId));
|
||||
return modelTargetFormat(findModel(models, modelId, aliasOrId));
|
||||
}
|
||||
|
||||
export function getModelType(aliasOrId, modelId) {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
if (!models) return null;
|
||||
const found = models.find(m => m.id === modelId);
|
||||
const found = findModel(models, modelId, aliasOrId);
|
||||
return found?.kind || found?.type || null;
|
||||
}
|
||||
|
||||
export function getModelUpstreamId(aliasOrId, modelId) {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
const found = models?.find(m => m.id === modelId);
|
||||
const found = findModel(models, modelId, aliasOrId);
|
||||
if (found?.upstreamModelId) return found.upstreamModelId;
|
||||
if (found?.id) return found.id;
|
||||
if (aliasOrId === "cx" && typeof modelId === "string" && modelId.endsWith(CODEX_REVIEW_SUFFIX)) {
|
||||
return modelId.slice(0, -CODEX_REVIEW_SUFFIX.length);
|
||||
}
|
||||
@@ -57,7 +75,7 @@ export function getModelUpstreamId(aliasOrId, modelId) {
|
||||
|
||||
export function getModelQuotaFamily(aliasOrId, modelId) {
|
||||
const models = PROVIDER_MODELS[aliasOrId];
|
||||
return modelQuotaFamily(models?.find(m => m.id === modelId));
|
||||
return modelQuotaFamily(findModel(models, modelId, aliasOrId));
|
||||
}
|
||||
|
||||
// OAuth short aliases — derived from registry `alias` (single source). everything else: alias = id.
|
||||
@@ -79,5 +97,5 @@ export function getModelsByProviderId(providerId) {
|
||||
// Get strip list for a model entry (explicit opt-in only)
|
||||
// Returns array of content types to strip, e.g. ["image", "audio"]
|
||||
export function getModelStrip(alias, modelId) {
|
||||
return modelStrip(PROVIDER_MODELS[alias]?.find(m => m.id === modelId));
|
||||
return modelStrip(findModel(PROVIDER_MODELS[alias], modelId, alias));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { deriveModelName } from "./namePatterns.js";
|
||||
|
||||
// Normalize version separators in a model id: hyphen between two digits becomes a dot.
|
||||
// Registry ids use dots for versions ("claude-sonnet-4.5") but clients (CLIs, aliases)
|
||||
// often send them with dashes ("claude-sonnet-4-5"). Only digit-digit hyphens are
|
||||
// touched, so word/suffix hyphens stay intact ("-thinking", "-agentic", "qwen3-coder-next").
|
||||
export function normalizeModelId(modelId) {
|
||||
if (typeof modelId !== "string") return modelId;
|
||||
return modelId.replace(/(\d)-(\d)/g, "$1.$2");
|
||||
}
|
||||
|
||||
// Model defaults centralized (was scattered as `m.kind || "llm"`, `quotaFamily || "normal"`, etc.)
|
||||
export const MODEL_DEFAULTS = {
|
||||
kind: "llm",
|
||||
|
||||
@@ -42,19 +42,38 @@ export default {
|
||||
},
|
||||
},
|
||||
models: [
|
||||
// Opus (added per kiro.dev/changelog/models and kiro.dev/docs/models)
|
||||
{ id: "claude-opus-4.8", name: "Claude Opus 4.8" },
|
||||
{ id: "claude-opus-4.8-thinking", name: "Claude Opus 4.8 (Thinking)" },
|
||||
{ id: "claude-opus-4.8-agentic", name: "Claude Opus 4.8 (Agentic)" },
|
||||
{ id: "claude-opus-4.8-thinking-agentic", name: "Claude Opus 4.8 (Thinking + Agentic)" },
|
||||
{ id: "claude-opus-4.7", name: "Claude Opus 4.7" },
|
||||
{ id: "claude-opus-4.7-thinking", name: "Claude Opus 4.7 (Thinking)" },
|
||||
{ id: "claude-opus-4.7-agentic", name: "Claude Opus 4.7 (Agentic)" },
|
||||
{ id: "claude-opus-4.7-thinking-agentic", name: "Claude Opus 4.7 (Thinking + Agentic)" },
|
||||
{ id: "claude-opus-4.5", name: "Claude Opus 4.5" },
|
||||
{ id: "claude-opus-4.5-thinking", name: "Claude Opus 4.5 (Thinking)" },
|
||||
{ id: "claude-opus-4.5-agentic", name: "Claude Opus 4.5 (Agentic)" },
|
||||
{ id: "claude-opus-4.5-thinking-agentic", name: "Claude Opus 4.5 (Thinking + Agentic)" },
|
||||
// Sonnet
|
||||
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
|
||||
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
|
||||
// Haiku
|
||||
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
|
||||
// Non-Anthropic
|
||||
{ id: "deepseek-3.2", name: "DeepSeek 3.2", strip: ["image","audio"] },
|
||||
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next", strip: ["image","audio"] },
|
||||
{ id: "glm-5", name: "GLM 5" },
|
||||
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
|
||||
// Thinking variants
|
||||
{ id: "claude-sonnet-5-thinking", name: "Claude Sonnet 5 (Thinking)" },
|
||||
{ id: "claude-sonnet-4.5-thinking", name: "Claude Sonnet 4.5 (Thinking)" },
|
||||
{ id: "claude-haiku-4.5-thinking", name: "Claude Haiku 4.5 (Thinking)" },
|
||||
// Agentic variants
|
||||
{ id: "claude-sonnet-5-agentic", name: "Claude Sonnet 5 (Agentic)" },
|
||||
{ id: "claude-sonnet-4.5-agentic", name: "Claude Sonnet 4.5 (Agentic)" },
|
||||
{ id: "claude-haiku-4.5-agentic", name: "Claude Haiku 4.5 (Agentic)" },
|
||||
// Thinking + Agentic variants
|
||||
{ id: "claude-sonnet-5-thinking-agentic", name: "Claude Sonnet 5 (Thinking + Agentic)" },
|
||||
{ id: "claude-sonnet-4.5-thinking-agentic", name: "Claude Sonnet 4.5 (Thinking + Agentic)" },
|
||||
{ id: "claude-haiku-4.5-thinking-agentic", name: "Claude Haiku 4.5 (Thinking + Agentic)" },
|
||||
|
||||
@@ -404,7 +404,10 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
|
||||
|
||||
let finalContent = currentMessage?.userInputMessage?.content || "";
|
||||
|
||||
// System prompt → prepend to the user content.
|
||||
// System prompt: pass via native systemInstruction field (Kiro/Q API supports it)
|
||||
// and also prepend as <instructions> in user content as fallback for upstreams
|
||||
// that don't support the native field.
|
||||
let systemInstruction = undefined;
|
||||
if (body.system) {
|
||||
let systemText = "";
|
||||
if (typeof body.system === "string") {
|
||||
@@ -412,7 +415,10 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
|
||||
} else if (Array.isArray(body.system)) {
|
||||
systemText = body.system.map((s) => s.text || "").join("\n");
|
||||
}
|
||||
if (systemText) finalContent = `${systemText}\n\n${finalContent}`;
|
||||
if (systemText) {
|
||||
systemInstruction = systemText;
|
||||
finalContent = `<instructions>\n${systemText}\n</instructions>\n\n${finalContent}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix order: thinking_mode tag, timestamp marker, then agentic prompt.
|
||||
@@ -423,23 +429,29 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
|
||||
if (agentic) prefixParts.push(KIRO_AGENTIC_SYSTEM_PROMPT);
|
||||
finalContent = `${prefixParts.join("\n\n")}\n\n${finalContent}`;
|
||||
|
||||
const userInputMessage = {
|
||||
content: finalContent,
|
||||
modelId: upstreamModel,
|
||||
origin: "AI_EDITOR",
|
||||
...(currentMessage?.userInputMessage?.userInputMessageContext && {
|
||||
userInputMessageContext:
|
||||
currentMessage.userInputMessage.userInputMessageContext,
|
||||
}),
|
||||
...(currentMessage?.userInputMessage?.images && {
|
||||
images: currentMessage.userInputMessage.images,
|
||||
}),
|
||||
};
|
||||
|
||||
if (systemInstruction) {
|
||||
userInputMessage.systemInstruction = systemInstruction;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
conversationState: {
|
||||
chatTriggerType: "MANUAL",
|
||||
conversationId: uuidv4(),
|
||||
currentMessage: {
|
||||
userInputMessage: {
|
||||
content: finalContent,
|
||||
modelId: upstreamModel,
|
||||
origin: "AI_EDITOR",
|
||||
...(currentMessage?.userInputMessage?.userInputMessageContext && {
|
||||
userInputMessageContext:
|
||||
currentMessage.userInputMessage.userInputMessageContext,
|
||||
}),
|
||||
...(currentMessage?.userInputMessage?.images && {
|
||||
images: currentMessage.userInputMessage.images,
|
||||
}),
|
||||
},
|
||||
userInputMessage,
|
||||
},
|
||||
history,
|
||||
},
|
||||
|
||||
@@ -129,14 +129,15 @@ function fixMissingToolResponsesOpenAI(messages) {
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap mid-conversation system text so it ends as a user turn (avoids Anthropic prefill 400)
|
||||
// Wrap mid-conversation system text so it ends as a user turn (avoids Anthropic prefill 400).
|
||||
// Uses <instructions> tags that Claude models treat as authoritative directives.
|
||||
function systemReminderText(content) {
|
||||
const parts = Array.isArray(content)
|
||||
? content.filter(c => c?.type === CLAUDE_BLOCK.TEXT).map(c => c.text || "")
|
||||
: [typeof content === "string" ? content : ""];
|
||||
const text = parts.filter(Boolean).join("\n");
|
||||
if (!text.trim()) return "";
|
||||
return `<system-reminder>\n${text}\n</system-reminder>`;
|
||||
return `<instructions>\n${text}\n</instructions>`;
|
||||
}
|
||||
|
||||
// Convert single Claude message - returns single message or array of messages
|
||||
|
||||
@@ -270,6 +270,7 @@ function convertMessages(messages, tools, model) {
|
||||
let role = msg.role;
|
||||
|
||||
// Normalize: system/tool -> user
|
||||
const wasSystem = role === ROLE.SYSTEM;
|
||||
if (role === ROLE.SYSTEM || role === ROLE.TOOL) {
|
||||
role = ROLE.USER;
|
||||
}
|
||||
@@ -338,7 +339,10 @@ function convertMessages(messages, tools, model) {
|
||||
content: [{ text: toolContent }]
|
||||
});
|
||||
} else if (content) {
|
||||
pendingUserContent.push(content);
|
||||
// <instructions> tags: Claude models treat these as authoritative directives.
|
||||
pendingUserContent.push(
|
||||
wasSystem ? `<instructions>\n${content}\n</instructions>` : content
|
||||
);
|
||||
}
|
||||
} else if (role === ROLE.ASSISTANT) {
|
||||
// Extract text content and tool uses
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Guards C2: regex name fallback (no catalog). Terse entries derive name; existing names untouched.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { deriveModelName } from "../../open-sse/providers/models/namePatterns.js";
|
||||
import { normalizeModel } from "../../open-sse/providers/models/schema.js";
|
||||
import { normalizeModel, normalizeModelId } from "../../open-sse/providers/models/schema.js";
|
||||
|
||||
describe("model name regex fallback (C2)", () => {
|
||||
it("derives display name from id per family", () => {
|
||||
@@ -25,4 +25,26 @@ describe("model name regex fallback (C2)", () => {
|
||||
expect(m.id).toBe("glm-5");
|
||||
expect(m.name).toBe("GLM 5");
|
||||
});
|
||||
|
||||
it("normalizeModelId: dash between digits becomes a dot (version separator)", () => {
|
||||
expect(normalizeModelId("claude-sonnet-4-5")).toBe("claude-sonnet-4.5");
|
||||
expect(normalizeModelId("minimax-m2-5")).toBe("minimax-m2.5");
|
||||
expect(normalizeModelId("deepseek-3-2")).toBe("deepseek-3.2");
|
||||
});
|
||||
|
||||
it("normalizeModelId: preserves word-suffix hyphens (-thinking, -agentic)", () => {
|
||||
expect(normalizeModelId("claude-sonnet-4-5-thinking")).toBe("claude-sonnet-4.5-thinking");
|
||||
expect(normalizeModelId("claude-sonnet-4-5-thinking-agentic")).toBe("claude-sonnet-4.5-thinking-agentic");
|
||||
});
|
||||
|
||||
it("normalizeModelId: leaves ids with no digit-digit hyphen untouched", () => {
|
||||
expect(normalizeModelId("qwen3-coder-next")).toBe("qwen3-coder-next");
|
||||
expect(normalizeModelId("claude-sonnet-5")).toBe("claude-sonnet-5");
|
||||
expect(normalizeModelId("glm-5")).toBe("glm-5");
|
||||
});
|
||||
|
||||
it("normalizeModelId: non-string input passes through", () => {
|
||||
expect(normalizeModelId(undefined)).toBeUndefined();
|
||||
expect(normalizeModelId(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user