refactor(open-sse): translator DRY + schema enums, bug fixes, dead code cleanup

- Bug B1-B7: media UI m.kind||m.type, serviceKinds, gemini mediaPriority, schema kind, models/info lookup by kind
- Dead code D1-D6: safeParseJSON, drop PROVIDER_ENDPOINTS, orphan fetcher, GITHUB_CONFIG derive, getProviderConfig internal, legacy kiro file
- Translator concerns: toOpenAIUsage, toOpenAIFinish (gemini/kiro/ollama + fix kiro tool finish), thinking effort maps
- Reorg helpers/ → concerns/ (logic) + formats/ (per-format) + schema/ (pure enums: roles/blocks/finishReasons/defaults)
- Wire ~280 hardcoded role/block/finish/default literals to schema enums across 20+ files
- collapseTextParts + extractTextContent dedup
- Normalize translator fn names to openaiToXRequest / xToOpenAIResponse
- Golden tests lock behavior; 0 regression (byte-for-byte providers/alias, 26=26 known fails)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-14 18:49:38 +07:00
co-authored by Cursor
parent c5c9061eac
commit d3f61aac2f
145 changed files with 1252 additions and 1160 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import { OAUTH_ENDPOINTS, ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER, AG_DEFAU
import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { deriveSessionId } from "../utils/sessionManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { cleanJSONSchemaForAntigravity } from "../translator/helpers/geminiHelper.js";
import { cleanJSONSchemaForAntigravity } from "../translator/formats/gemini.js";
// Sanitize function name: Gemini requires [a-zA-Z_][a-zA-Z0-9_.:\-]{0,63}
function sanitizeFunctionName(name) {
+2 -2
View File
@@ -6,8 +6,8 @@ import {
refreshProviderCredentials,
shouldRefreshCredentials,
} from "../services/oauthCredentialManager.js";
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";
import { fetchImageAsBase64 } from "../translator/helpers/imageHelper.js";
import { normalizeResponsesInput } from "../translator/formats/responsesApi.js";
import { fetchImageAsBase64 } from "../translator/concerns/image.js";
import { getModelUpstreamId } from "../config/providerModels.js";
import { getConsistentMachineId } from "../shared/machineId.js";
import { DEFAULT_RETRY_CONFIG, resolveRetryEntry } from "../config/runtimeConfig.js";
+3 -3
View File
@@ -1,7 +1,7 @@
import { randomUUID } from "crypto";
import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { convertCommandCodeToOpenAI } from "../translator/response/commandcode-to-openai.js";
import { commandCodeToOpenAIResponse } from "../translator/response/commandcode-to-openai.js";
import { SSE_DONE } from "../utils/sseConstants.js";
/**
@@ -71,13 +71,13 @@ function wrapNdjsonAsOpenAISse(originalResponse, model) {
const trimmed = line.trim();
if (!trimmed) continue;
// Translate AI SDK v5 NDJSON line to one or more OpenAI chunks
emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller);
emitChunks(commandCodeToOpenAIResponse(trimmed, state), controller);
}
},
flush(controller) {
const trimmed = buffer.trim();
if (trimmed) {
emitChunks(convertCommandCodeToOpenAI(trimmed, state), controller);
emitChunks(commandCodeToOpenAIResponse(trimmed, state), controller);
}
controller.enqueue(encoder.encode(SSE_DONE));
},
+1 -1
View File
@@ -142,7 +142,7 @@ export class CursorExecutor extends BaseExecutor {
transformRequest(model, body, stream, credentials) {
// Messages are already translated by chatCore (claude→openai→cursor)
// Do NOT call buildCursorRequest again — double-translation drops tool_results
// Do NOT call openaiToCursorRequest again — double-translation drops tool_results
const messages = body.messages || [];
const tools = body.tools || [];
const reasoningEffort = body.reasoning_effort || null;
+1 -1
View File
@@ -1,7 +1,7 @@
import { detectFormat, getTargetFormat } from "../services/provider.js";
import { translateRequest } from "../translator/index.js";
import { FORMATS } from "../translator/formats.js";
import { normalizeClaudePassthrough } from "../translator/helpers/claudeHelper.js";
import { normalizeClaudePassthrough } from "../translator/formats/claude.js";
import { COLORS } from "../utils/stream.js";
import { createStreamController } from "../utils/streamHandler.js";
import { refreshWithRetry } from "../services/tokenRefresh.js";
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { handleChatCore } from "./chatCore.js";
import { convertResponsesApiFormat } from "../translator/helpers/responsesApiHelper.js";
import { convertResponsesApiFormat } from "../translator/formats/responsesApi.js";
import { createResponsesApiTransformStream } from "../transformer/responsesTransformer.js";
import { convertResponsesStreamToJson } from "../transformer/streamToJsonConverter.js";
import { SSE_HEADERS_CORS } from "../utils/sseConstants.js";
-1
View File
@@ -30,7 +30,6 @@ export {
// Services
export {
detectFormat,
getProviderConfig,
getTargetFormat
} from "./services/provider.js";
+5 -5
View File
@@ -1,8 +1,8 @@
import { deriveModelName } from "./namePatterns.js";
// Model defaults centralized (was scattered as `m.type || "llm"`, `quotaFamily || "normal"`, etc.)
// Model defaults centralized (was scattered as `m.kind || "llm"`, `quotaFamily || "normal"`, etc.)
export const MODEL_DEFAULTS = {
type: "llm",
kind: "llm",
quotaFamily: "normal",
strip: [],
targetFormat: null
@@ -16,9 +16,9 @@ export function normalizeModel(raw) {
return { ...model, name: deriveModelName(model.id) };
}
// Resolve a single field with its default (keeps accessor call-sites one-liners)
export function modelType(model) {
return model?.type || MODEL_DEFAULTS.type;
// Resolve model kind with default (accepts legacy `type` field)
export function modelKind(model) {
return model?.kind || model?.type || MODEL_DEFAULTS.kind;
}
export function modelQuotaFamily(model) {
return model?.quotaFamily || MODEL_DEFAULTS.quotaFamily;
@@ -1,5 +1,6 @@
export default {
id: "alicode-intl",
priority: 10,
alias: "alicode-intl",
display: {
name: "Alibaba Intl",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "alicode",
priority: 20,
alias: "alicode",
display: {
name: "Alibaba",
+1
View File
@@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js";
export default {
id: "anthropic",
priority: 30,
alias: "anthropic",
display: {
name: "Anthropic",
@@ -3,6 +3,7 @@ import { ANTIGRAVITY_OAUTH_CLIENT } from "../shared.js";
export default {
id: "antigravity",
priority: 20,
alias: "ag",
uiAlias: "ag",
display: {
@@ -1,5 +1,6 @@
export default {
id: "assemblyai",
priority: 30,
alias: "assemblyai",
aliases: [
"aai",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "azure",
priority: 40,
alias: "azure",
display: {
name: "Azure OpenAI",
@@ -1,5 +1,6 @@
export default {
id: "black-forest-labs",
priority: 50,
alias: "black-forest-labs",
aliases: [
"bfl",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "blackbox",
priority: 50,
alias: "blackbox",
aliases: [
"bb",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "byteplus",
priority: 150,
alias: "byteplus",
aliases: [
"bpm",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "cerebras",
priority: 60,
alias: "cerebras",
display: {
name: "Cerebras",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "chutes",
priority: 70,
alias: "chutes",
aliases: [
"ch",
+1
View File
@@ -2,6 +2,7 @@ import { CLAUDE_CLI_SPOOF_HEADERS } from "../shared.js";
export default {
id: "claude",
priority: 10,
alias: "cc",
uiAlias: "cc",
display: {
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "cline",
priority: 70,
alias: "cl",
uiAlias: "cl",
display: {
@@ -1,5 +1,7 @@
export default {
id: "cloudflare-ai",
priority: 20,
hasFree: true,
alias: "cloudflare-ai",
aliases: [
"cf",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "codebuddy",
priority: 80,
display: {
name: "CodeBuddy",
icon: "smart_toy",
+1
View File
@@ -2,6 +2,7 @@ import { withCodexReviewModels } from "../models/helpers.js";
export default {
id: "codex",
priority: 30,
alias: "cx",
uiAlias: "cx",
display: {
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "cohere",
priority: 90,
alias: "cohere",
display: {
name: "Cohere",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "comfyui",
priority: 120,
alias: "comfyui",
display: {
name: "ComfyUI",
@@ -1,5 +1,6 @@
export default {
id: "commandcode",
priority: 100,
alias: "commandcode",
aliases: [
"cmc",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "cursor",
priority: 40,
alias: "cu",
uiAlias: "cu",
display: {
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "deepgram",
priority: 20,
alias: "deepgram",
aliases: [
"dg",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "deepseek",
priority: 110,
alias: "deepseek",
aliases: [
"ds",
+2
View File
@@ -1,5 +1,7 @@
export default {
id: "fal-ai",
priority: 90,
hasFree: true,
alias: "fal-ai",
aliases: [
"fal",
+2
View File
@@ -1,5 +1,6 @@
export default {
id: "fireworks",
priority: 50,
alias: "fireworks",
display: {
name: "Fireworks AI",
@@ -23,5 +24,6 @@ export default {
{ id: "accounts/fireworks/models/qwen3-235b-a22b", name: "Qwen3 235B" },
{ id: "nomic-ai/nomic-embed-text-v1.5", name: "Nomic Embed Text v1.5", kind: "embedding" },
],
serviceKinds: ["llm", "embedding"],
embeddingConfig: { baseUrl: "https://api.fireworks.ai/inference/v1/embeddings" },
};
@@ -2,6 +2,8 @@ import { GOOGLE_OAUTH_CLIENT } from "../shared.js";
export default {
id: "gemini-cli",
priority: 130,
hasFree: true,
alias: "gc",
uiAlias: "gc",
display: {
+3 -1
View File
@@ -2,6 +2,8 @@ import { GOOGLE_OAUTH_CLIENT } from "../shared.js";
export default {
id: "gemini",
priority: 10,
hasFree: true,
alias: "gemini",
display: {
name: "Gemini",
@@ -12,9 +14,9 @@ export default {
notice: {
apiKeyUrl: "https://aistudio.google.com/app/apikey",
},
mediaPriority: 1,
},
category: "freeTier",
mediaPriority: 1,
transport: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
format: "gemini",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "github",
priority: 50,
alias: "gh",
uiAlias: "gh",
display: {
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "gitlab",
priority: 120,
display: {
name: "GitLab Duo",
icon: "code",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "glm-cn",
priority: 130,
alias: "glm-cn",
display: {
name: "GLM (China)",
+1
View File
@@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js";
export default {
id: "glm",
priority: 140,
alias: "glm",
display: {
name: "GLM Coding",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "grok-web",
priority: 150,
alias: "grok-web",
aliases: [
"gw",
+2
View File
@@ -1,5 +1,7 @@
export default {
id: "groq",
priority: 60,
hasFree: true,
alias: "groq",
display: {
name: "Groq",
@@ -1,5 +1,7 @@
export default {
id: "huggingface",
priority: 70,
hasFree: true,
alias: "huggingface",
aliases: [
"hf",
@@ -27,5 +29,6 @@ export default {
{ id: "openai/whisper-large-v3", name: "Whisper Large v3 (HF)", params: ["language"], kind: "stt" },
{ id: "openai/whisper-small", name: "Whisper Small (HF)", params: ["language"], kind: "stt" },
],
serviceKinds: ["image", "stt"],
imageConfig: { baseUrl: "https://api-inference.huggingface.co/models" },
};
@@ -1,5 +1,6 @@
export default {
id: "hyperbolic",
priority: 160,
alias: "hyperbolic",
aliases: [
"hyp",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "iflow",
priority: 170,
alias: "if",
display: {
name: "iFlow AI",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "kilocode",
priority: 60,
alias: "kc",
uiAlias: "kc",
display: {
@@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS, KIMI_CODING_BASE_URL } from "../shared.js";
export default {
id: "kimi-coding",
priority: 180,
alias: "kmc",
display: {
name: "Kimi Coding",
+1
View File
@@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS, KIMI_CODING_BASE_URL } from "../shared.js";
export default {
id: "kimi",
priority: 170,
alias: "kimi",
display: {
name: "Kimi",
+1
View File
@@ -1,5 +1,6 @@
export default {
id: "kiro",
priority: 80,
alias: "kr",
uiAlias: "kr",
display: {
@@ -1,271 +0,0 @@
/**
* migrate-registry.mjs
* Migrates all registry files to Model-A schema:
* - models[] = ALL models (chat + media), field `kind` (default "llm")
* - media wrapper removed → fields promoted top-level
* - *Config.models removed (data merged into models[])
* - format: terse, consistent indent
*
* Run: node --experimental-vm-modules migrate-registry.mjs [--dry]
*/
import { readFileSync, writeFileSync, readdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { createRequire } from "node:module";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REGISTRY_DIR = __dirname; // script lives in registry/
const DRY = process.argv.includes("--dry");
// *Config.models field → kind value
const CFG_KIND = {
ttsConfig: "tts",
sttConfig: "stt",
embeddingConfig: "embedding",
imageConfig: "image",
imageToTextConfig: "imageToText",
videoConfig: "video",
musicConfig: "music",
};
// Fields in *Config that are NOT models (keep on config)
const MODEL_ONLY_KEY = "models";
// Top-level registry fields that are NOT media-config (don't flatten these from media)
// serviceKinds + *Config + searchViaChat + mediaConfig + passthroughModels are media fields
// Everything else is already top-level
const MEDIA_WHITELIST = new Set([
"serviceKinds",
"ttsConfig", "sttConfig", "embeddingConfig",
"imageConfig", "imageToTextConfig", "videoConfig", "musicConfig",
"searchViaChat", "searchConfig", "fetchConfig",
"modelsFetcher", "hasProviderSpecificData", "passthroughModels",
"mediaPriority", "hiddenKinds",
]);
function migrateEntry(entry, filename) {
const out = {};
// 1. Top-level identity/transport fields (preserve order)
const TRANSPORT_KEYS = ["id", "alias", "aliases", "uiAlias", "display", "category",
"authType", "authHint", "authModes", "hasOAuth", "noAuth",
"hasProviderSpecificData", "thinkingConfig", "hiddenKinds",
"regions", "defaultRegion", "passthroughModels", "transport"];
for (const k of TRANSPORT_KEYS) {
if (entry[k] !== undefined) out[k] = entry[k];
}
// 2. Collect existing models[] (convert type→kind, skip if kind already set)
const existingModels = (entry.models || []).map(m => {
const { type, ...rest } = m;
const kind = m.kind ?? (type && type !== "llm" ? type : undefined);
return kind ? { ...rest, kind } : rest;
});
const existingIds = new Set(existingModels.map(m => m.id));
// 3. Extract models from *Config.models (merge into models[])
const mediaModels = [];
const media = entry.media || {};
for (const [cfgKey, kind] of Object.entries(CFG_KIND)) {
const cfg = media[cfgKey];
if (!cfg?.models) continue;
for (const m of cfg.models) {
// Check if same id+kind combo already exists to avoid true duplicates
const dup = existingModels.find(x => x.id === m.id && (x.kind ?? "llm") === kind);
if (dup) continue;
const { ...mClean } = m;
mediaModels.push({ ...mClean, kind });
}
}
// 4. Merge models (existing first, then media additions)
const allModels = [...existingModels, ...mediaModels];
// Only include models key if non-empty or explicitly defined
if (allModels.length > 0 || entry.models !== undefined) {
out.models = allModels;
}
// 5. Flatten media fields (without .models sub-arrays)
for (const [k, v] of Object.entries(media)) {
if (!MEDIA_WHITELIST.has(k)) continue;
if (CFG_KIND[k]) {
// Strip .models from config, keep rest
const { models: _m, ...cfgRest } = (v || {});
if (Object.keys(cfgRest).length > 0) out[k] = cfgRest;
} else {
out[k] = v;
}
}
// 6. Other top-level fields not in TRANSPORT_KEYS and not media (e.g. features, oauth, usage in transport)
const SKIP = new Set([...TRANSPORT_KEYS, "models", "media", ...Object.keys(CFG_KIND),
"serviceKinds", "searchViaChat", "searchConfig", "fetchConfig",
"modelsFetcher", "passthroughModels", "mediaPriority"]);
for (const [k, v] of Object.entries(entry)) {
if (!SKIP.has(k)) out[k] = v;
}
return out;
}
// Format a registry entry as clean JS (no JSON.stringify — write proper ES module)
function formatValue(v, indent = 0) {
const pad = " ".repeat(indent);
const pad1 = " ".repeat(indent + 1);
if (v === null || v === undefined) return String(v);
if (typeof v === "boolean" || typeof v === "number") return String(v);
if (typeof v === "string") return JSON.stringify(v);
if (Array.isArray(v)) {
if (v.length === 0) return "[]";
// Model arrays: 1 model per line (compact inline object)
const items = v.map(item => {
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
return `${pad1}${formatInlineObject(item)}`;
}
return `${pad1}${formatValue(item, indent + 1)}`;
});
return `[\n${items.join(",\n")},\n${pad}]`;
}
if (typeof v === "object") {
const keys = Object.keys(v);
if (keys.length === 0) return "{}";
const lines = keys.map(k => {
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
return `${pad1}${key}: ${formatValue(v[k], indent + 1)}`;
});
return `{\n${lines.join(",\n")},\n${pad}}`;
}
return JSON.stringify(v);
}
// Inline compact object: { id: "x", name: "y", kind: "tts", dimensions: 1536 }
function formatInlineObject(obj) {
const parts = Object.entries(obj).map(([k, v]) => {
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
return `${key}: ${JSON.stringify(v)}`;
});
return `{ ${parts.join(", ")} }`;
}
// Config objects (ttsConfig etc) — inline single line if short, else multi-line
function formatConfig(cfg) {
const line = `{ ${Object.entries(cfg).map(([k,v])=>`${k}: ${JSON.stringify(v)}`).join(", ")} }`;
if (line.length <= 120) return line;
const pad1 = " ".repeat(2);
const lines = Object.entries(cfg).map(([k,v]) => `${pad1}${k}: ${JSON.stringify(v)}`);
return `{\n${lines.join(",\n")},\n }`;
}
// Top-level registry entry formatter
function formatEntry(entry, imports = "") {
const lines = [];
if (imports) lines.push(imports, "");
lines.push("export default {");
const TOP_ORDER = [
"id", "alias", "aliases", "uiAlias", "display", "category",
"authType", "authHint", "authModes", "hasOAuth", "noAuth",
"hasProviderSpecificData", "thinkingConfig", "hiddenKinds",
"regions", "defaultRegion", "transport",
"models",
// media fields
"serviceKinds",
"ttsConfig", "sttConfig", "embeddingConfig",
"imageConfig", "imageToTextConfig", "videoConfig", "musicConfig",
"searchViaChat", "searchConfig", "fetchConfig", "modelsFetcher",
"passthroughModels", "mediaPriority",
// other
"oauth", "features",
];
const emitted = new Set();
function emitKey(k) {
if (!(k in entry) || emitted.has(k)) return;
emitted.add(k);
const v = entry[k];
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
// Config objects (xConfig) — special inline format
if (CFG_KIND[k] || k === "searchViaChat" || k === "searchConfig" || k === "fetchConfig" || k === "modelsFetcher") {
lines.push(` ${key}: ${formatConfig(v)},`);
return;
}
// models[] — terse per-line
if (k === "models" && Array.isArray(v)) {
if (v.length === 0) { lines.push(` models: [],`); return; }
lines.push(` models: [`);
for (const m of v) lines.push(` ${formatInlineObject(m)},`);
lines.push(` ],`);
return;
}
// serviceKinds — inline array
if (k === "serviceKinds") {
lines.push(` serviceKinds: ${JSON.stringify(v)},`);
return;
}
// display — multi-line
if (k === "display") {
lines.push(` display: ${formatValue(v, 1)},`);
return;
}
// transport — multi-line
if (k === "transport") {
lines.push(` transport: ${formatValue(v, 1)},`);
return;
}
// Everything else
lines.push(` ${key}: ${formatValue(v, 1)},`);
}
for (const k of TOP_ORDER) emitKey(k);
// Emit any remaining keys not in TOP_ORDER
for (const k of Object.keys(entry)) emitKey(k);
lines.push("};");
return lines.join("\n") + "\n";
}
// --- Main ---
const files = readdirSync(REGISTRY_DIR).filter(f => f.endsWith(".js") && f !== "index.js");
let count = 0;
for (const file of files) {
const path = join(REGISTRY_DIR, file);
const src = readFileSync(path, "utf8");
// Extract import lines (for files that import shared constants)
const importLines = src.split("\n").filter(l => l.startsWith("import "));
const importSrc = importLines.join("\n");
// Dynamic import to get entry
let entry;
try {
const mod = await import(`${join(REGISTRY_DIR, file)}?t=${Date.now()}`);
entry = mod.default;
} catch (e) {
console.error(`SKIP ${file}: ${e.message}`);
continue;
}
const migrated = migrateEntry(entry, file);
const output = formatEntry(migrated, importSrc);
if (DRY) {
console.log(`\n=== ${file} ===\n${output}`);
} else {
writeFileSync(path, output, "utf8");
count++;
}
}
console.log(DRY ? `[DRY] Would migrate ${files.length} files` : `✅ Migrated ${count} files`);
+2
View File
@@ -1,5 +1,7 @@
export default {
id: "mimo-free",
priority: 120,
hasFree: true,
alias: "mmf",
uiAlias: "mmf",
display: {
@@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js";
export default {
id: "minimax-cn",
priority: 190,
alias: "minimax-cn",
display: {
name: "Minimax (China)",

Some files were not shown because too many files have changed in this diff Show More