mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 05:31:47 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -30,7 +30,6 @@ export {
|
||||
// Services
|
||||
export {
|
||||
detectFormat,
|
||||
getProviderConfig,
|
||||
getTargetFormat
|
||||
} from "./services/provider.js";
|
||||
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
export default {
|
||||
id: "alicode",
|
||||
priority: 20,
|
||||
alias: "alicode",
|
||||
display: {
|
||||
name: "Alibaba",
|
||||
|
||||
@@ -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,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,5 +1,6 @@
|
||||
export default {
|
||||
id: "blackbox",
|
||||
priority: 50,
|
||||
alias: "blackbox",
|
||||
aliases: [
|
||||
"bb",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "byteplus",
|
||||
priority: 150,
|
||||
alias: "byteplus",
|
||||
aliases: [
|
||||
"bpm",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "cerebras",
|
||||
priority: 60,
|
||||
alias: "cerebras",
|
||||
display: {
|
||||
name: "Cerebras",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "chutes",
|
||||
priority: 70,
|
||||
alias: "chutes",
|
||||
aliases: [
|
||||
"ch",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CLAUDE_CLI_SPOOF_HEADERS } from "../shared.js";
|
||||
|
||||
export default {
|
||||
id: "claude",
|
||||
priority: 10,
|
||||
alias: "cc",
|
||||
uiAlias: "cc",
|
||||
display: {
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
export default {
|
||||
id: "codebuddy",
|
||||
priority: 80,
|
||||
display: {
|
||||
name: "CodeBuddy",
|
||||
icon: "smart_toy",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { withCodexReviewModels } from "../models/helpers.js";
|
||||
|
||||
export default {
|
||||
id: "codex",
|
||||
priority: 30,
|
||||
alias: "cx",
|
||||
uiAlias: "cx",
|
||||
display: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "cohere",
|
||||
priority: 90,
|
||||
alias: "cohere",
|
||||
display: {
|
||||
name: "Cohere",
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
export default {
|
||||
id: "cursor",
|
||||
priority: 40,
|
||||
alias: "cu",
|
||||
uiAlias: "cu",
|
||||
display: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "deepgram",
|
||||
priority: 20,
|
||||
alias: "deepgram",
|
||||
aliases: [
|
||||
"dg",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "deepseek",
|
||||
priority: 110,
|
||||
alias: "deepseek",
|
||||
aliases: [
|
||||
"ds",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default {
|
||||
id: "fal-ai",
|
||||
priority: 90,
|
||||
hasFree: true,
|
||||
alias: "fal-ai",
|
||||
aliases: [
|
||||
"fal",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
export default {
|
||||
id: "github",
|
||||
priority: 50,
|
||||
alias: "gh",
|
||||
uiAlias: "gh",
|
||||
display: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "gitlab",
|
||||
priority: 120,
|
||||
display: {
|
||||
name: "GitLab Duo",
|
||||
icon: "code",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "glm-cn",
|
||||
priority: 130,
|
||||
alias: "glm-cn",
|
||||
display: {
|
||||
name: "GLM (China)",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js";
|
||||
|
||||
export default {
|
||||
id: "glm",
|
||||
priority: 140,
|
||||
alias: "glm",
|
||||
display: {
|
||||
name: "GLM Coding",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "grok-web",
|
||||
priority: 150,
|
||||
alias: "grok-web",
|
||||
aliases: [
|
||||
"gw",
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
export default {
|
||||
id: "iflow",
|
||||
priority: 170,
|
||||
alias: "if",
|
||||
display: {
|
||||
name: "iFlow AI",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
export default {
|
||||
id: "kiro",
|
||||
priority: 80,
|
||||
alias: "kr",
|
||||
uiAlias: "kr",
|
||||
display: {
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CLAUDE_API_HEADERS } from "../shared.js";
|
||||
|
||||
export default {
|
||||
id: "minimax",
|
||||
priority: 90,
|
||||
alias: "minimax",
|
||||
display: {
|
||||
name: "Minimax Coding",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "mistral",
|
||||
priority: 80,
|
||||
alias: "mistral",
|
||||
display: {
|
||||
name: "Mistral",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "mmf",
|
||||
priority: 200,
|
||||
display: {
|
||||
name: "MMF",
|
||||
icon: "hub",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default {
|
||||
id: "nanobanana",
|
||||
priority: 80,
|
||||
hasFree: true,
|
||||
alias: "nanobanana",
|
||||
aliases: [
|
||||
"nb",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "nebius",
|
||||
priority: 70,
|
||||
alias: "nebius",
|
||||
display: {
|
||||
name: "Nebius AI",
|
||||
@@ -21,5 +22,6 @@ export default {
|
||||
{ id: "meta-llama/Llama-3.3-70B-Instruct", name: "Llama 3.3 70B Instruct" },
|
||||
{ id: "Qwen/Qwen3-Embedding-8B", name: "Qwen3 Embedding 8B", kind: "embedding" },
|
||||
],
|
||||
serviceKinds: ["llm", "embedding"],
|
||||
embeddingConfig: { baseUrl: "https://api.tokenfactory.nebius.com/v1/embeddings" },
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default {
|
||||
id: "nvidia",
|
||||
priority: 100,
|
||||
hasFree: true,
|
||||
alias: "nvidia",
|
||||
display: {
|
||||
name: "NVIDIA NIM",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default {
|
||||
id: "ollama-local",
|
||||
priority: 50,
|
||||
hasFree: true,
|
||||
alias: "ollama-local",
|
||||
display: {
|
||||
name: "Ollama Local",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default {
|
||||
id: "ollama",
|
||||
priority: 40,
|
||||
hasFree: true,
|
||||
alias: "ollama",
|
||||
display: {
|
||||
name: "Ollama Cloud",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "openai",
|
||||
priority: 30,
|
||||
alias: "openai",
|
||||
display: {
|
||||
name: "OpenAI",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "opencode-go",
|
||||
priority: 210,
|
||||
alias: "opencode-go",
|
||||
aliases: [
|
||||
"ocg",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default {
|
||||
id: "opencode",
|
||||
priority: 110,
|
||||
hasFree: true,
|
||||
alias: "oc",
|
||||
uiAlias: "oc",
|
||||
display: {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default {
|
||||
id: "openrouter",
|
||||
priority: 30,
|
||||
hasFree: true,
|
||||
alias: "openrouter",
|
||||
display: {
|
||||
name: "OpenRouter",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "perplexity-web",
|
||||
priority: 220,
|
||||
alias: "perplexity-web",
|
||||
aliases: [
|
||||
"pw",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "perplexity",
|
||||
priority: 180,
|
||||
alias: "perplexity",
|
||||
aliases: [
|
||||
"pplx",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "qoder",
|
||||
priority: 230,
|
||||
alias: "qd",
|
||||
uiAlias: "qd",
|
||||
display: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "qwen",
|
||||
priority: 240,
|
||||
alias: "qw",
|
||||
display: {
|
||||
name: "Qwen Code",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "recraft",
|
||||
priority: 70,
|
||||
alias: "recraft",
|
||||
display: {
|
||||
name: "Recraft",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "runwayml",
|
||||
priority: 80,
|
||||
alias: "runwayml",
|
||||
aliases: [
|
||||
"runway",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "sdwebui",
|
||||
priority: 110,
|
||||
alias: "sdwebui",
|
||||
display: {
|
||||
name: "SD WebUI",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "siliconflow",
|
||||
priority: 250,
|
||||
alias: "siliconflow",
|
||||
display: {
|
||||
name: "SiliconFlow",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "stability-ai",
|
||||
priority: 60,
|
||||
alias: "stability-ai",
|
||||
aliases: [
|
||||
"stability",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "together",
|
||||
priority: 60,
|
||||
alias: "together",
|
||||
display: {
|
||||
name: "Together AI",
|
||||
@@ -25,5 +26,6 @@ export default {
|
||||
{ id: "BAAI/bge-large-en-v1.5", name: "BGE Large EN v1.5", kind: "embedding" },
|
||||
{ id: "togethercomputer/m2-bert-80M-8k-retrieval", name: "M2 BERT 80M 8K", kind: "embedding" },
|
||||
],
|
||||
serviceKinds: ["llm", "embedding"],
|
||||
embeddingConfig: { baseUrl: "https://api.together.xyz/v1/embeddings" },
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "vercel-ai-gateway",
|
||||
priority: 160,
|
||||
alias: "vercel-ai-gateway",
|
||||
aliases: [
|
||||
"vercel",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "vertex-partner",
|
||||
priority: 260,
|
||||
alias: "vertex-partner",
|
||||
aliases: [
|
||||
"vxp",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "vertex",
|
||||
priority: 140,
|
||||
alias: "vertex",
|
||||
aliases: [
|
||||
"vx",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "volcengine-ark",
|
||||
priority: 270,
|
||||
alias: "volcengine-ark",
|
||||
aliases: [
|
||||
"ark",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "voyage-ai",
|
||||
priority: 40,
|
||||
alias: "voyage-ai",
|
||||
uiAlias: "voyage",
|
||||
display: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "xai",
|
||||
priority: 280,
|
||||
alias: "xai",
|
||||
display: {
|
||||
name: "xAI (Grok)",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "xiaomi-mimo",
|
||||
priority: 290,
|
||||
alias: "xiaomi-mimo",
|
||||
aliases: [
|
||||
"mimo",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
id: "xiaomi-tokenplan",
|
||||
priority: 300,
|
||||
alias: "xiaomi-tokenplan",
|
||||
aliases: [
|
||||
"xmtp",
|
||||
|
||||
@@ -104,8 +104,8 @@ export function detectFormat(body) {
|
||||
return "openai";
|
||||
}
|
||||
|
||||
// Get provider config
|
||||
export function getProviderConfig(provider) {
|
||||
// Get provider config (internal — no external runtime consumer)
|
||||
function getProviderConfig(provider) {
|
||||
if (isOpenAICompatible(provider)) {
|
||||
const apiType = getOpenAICompatibleType(provider);
|
||||
return {
|
||||
|
||||
@@ -6,15 +6,15 @@ import { CLIENT_METADATA, getPlatformUserAgent } from "../config/appConstants.js
|
||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||
import { resolveDefaultProfileArn } from "../config/kiroConstants.js";
|
||||
import { ANTIGRAVITY_OAUTH_CLIENT, ANTHROPIC_API_VERSION } from "../providers/shared.js";
|
||||
import { PROVIDERS } from "../providers/index.js";
|
||||
import { PROVIDERS, PROVIDER_OAUTH } from "../providers/index.js";
|
||||
|
||||
// usage endpoints: single source from registry transport.usage
|
||||
const U = (id) => PROVIDERS[id]?.usage || {};
|
||||
|
||||
// GitHub API config
|
||||
// GitHub API config — single source from registry oauth block
|
||||
const GITHUB_CONFIG = {
|
||||
apiVersion: "2022-11-28",
|
||||
userAgent: "GitHubCopilotChat/0.26.7",
|
||||
apiVersion: PROVIDER_OAUTH.github?.apiVersion,
|
||||
userAgent: PROVIDER_OAUTH.github?.userAgent,
|
||||
};
|
||||
|
||||
// GLM quota endpoints (region-aware) — url from registry transport.usage
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Concern #6: finish_reason / stop_reason mapping.
|
||||
// One entry per direction; switch by special format, default handles common providers.
|
||||
import { OPENAI_FINISH, CLAUDE_STOP, GEMINI_FINISH } from "../schema/finishReasons.js";
|
||||
|
||||
// upstream finish/stop reason → OpenAI finish_reason
|
||||
export function toOpenAIFinish(reason, format) {
|
||||
switch (format) {
|
||||
case "claude":
|
||||
switch (reason) {
|
||||
case CLAUDE_STOP.END_TURN: return OPENAI_FINISH.STOP;
|
||||
case CLAUDE_STOP.MAX_TOKENS: return OPENAI_FINISH.LENGTH;
|
||||
case CLAUDE_STOP.TOOL_USE: return OPENAI_FINISH.TOOL_CALLS;
|
||||
case CLAUDE_STOP.STOP_SEQUENCE: return OPENAI_FINISH.STOP;
|
||||
default: return OPENAI_FINISH.STOP;
|
||||
}
|
||||
case "commandcode":
|
||||
switch (reason) {
|
||||
case "stop": return OPENAI_FINISH.STOP;
|
||||
case "length": return OPENAI_FINISH.LENGTH;
|
||||
case "tool-calls":
|
||||
case "tool_use": return OPENAI_FINISH.TOOL_CALLS;
|
||||
case "content-filter": return OPENAI_FINISH.CONTENT_FILTER;
|
||||
case "error": return OPENAI_FINISH.STOP;
|
||||
default: return reason || OPENAI_FINISH.STOP;
|
||||
}
|
||||
case "gemini":
|
||||
switch (String(reason).toUpperCase()) {
|
||||
case GEMINI_FINISH.STOP: return OPENAI_FINISH.STOP;
|
||||
case GEMINI_FINISH.MAX_TOKENS: return OPENAI_FINISH.LENGTH;
|
||||
case GEMINI_FINISH.SAFETY:
|
||||
case GEMINI_FINISH.RECITATION:
|
||||
case GEMINI_FINISH.BLOCKLIST:
|
||||
case GEMINI_FINISH.PROHIBITED_CONTENT: return OPENAI_FINISH.CONTENT_FILTER;
|
||||
default: return OPENAI_FINISH.STOP;
|
||||
}
|
||||
case "kiro":
|
||||
case "ollama":
|
||||
switch (reason) {
|
||||
case "tool_calls":
|
||||
case "tool_use": return OPENAI_FINISH.TOOL_CALLS;
|
||||
case "length":
|
||||
case "max_tokens": return OPENAI_FINISH.LENGTH;
|
||||
default: return OPENAI_FINISH.STOP;
|
||||
}
|
||||
default:
|
||||
return reason || OPENAI_FINISH.STOP;
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI finish_reason → upstream stop reason
|
||||
export function fromOpenAIFinish(reason, format) {
|
||||
switch (format) {
|
||||
case "claude":
|
||||
switch (reason) {
|
||||
case OPENAI_FINISH.STOP: return CLAUDE_STOP.END_TURN;
|
||||
case OPENAI_FINISH.LENGTH: return CLAUDE_STOP.MAX_TOKENS;
|
||||
case OPENAI_FINISH.TOOL_CALLS: return CLAUDE_STOP.TOOL_USE;
|
||||
default: return CLAUDE_STOP.END_TURN;
|
||||
}
|
||||
default:
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// Concern #6: finish_reason / stop_reason mapping.
|
||||
// One entry per direction; switch by special format, default handles common providers.
|
||||
|
||||
// upstream finish/stop reason → OpenAI finish_reason
|
||||
export function toOpenAIFinish(reason, format) {
|
||||
switch (format) {
|
||||
case "claude":
|
||||
switch (reason) {
|
||||
case "end_turn": return "stop";
|
||||
case "max_tokens": return "length";
|
||||
case "tool_use": return "tool_calls";
|
||||
case "stop_sequence": return "stop";
|
||||
default: return "stop";
|
||||
}
|
||||
case "commandcode":
|
||||
switch (reason) {
|
||||
case "stop": return "stop";
|
||||
case "length": return "length";
|
||||
case "tool-calls":
|
||||
case "tool_use": return "tool_calls";
|
||||
case "content-filter": return "content_filter";
|
||||
case "error": return "stop";
|
||||
default: return reason || "stop";
|
||||
}
|
||||
default:
|
||||
return reason || "stop";
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI finish_reason → upstream stop reason
|
||||
export function fromOpenAIFinish(reason, format) {
|
||||
switch (format) {
|
||||
case "claude":
|
||||
switch (reason) {
|
||||
case "stop": return "end_turn";
|
||||
case "length": return "max_tokens";
|
||||
case "tool_calls": return "tool_use";
|
||||
default: return "end_turn";
|
||||
}
|
||||
default:
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { OPENAI_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Collapse an OpenAI content-part array: a lone text part becomes a plain string,
|
||||
// otherwise the array is returned as-is. Matches existing translator behavior.
|
||||
export function collapseTextParts(parts) {
|
||||
return parts.length === 1 && parts[0].type === OPENAI_BLOCK.TEXT ? parts[0].text : parts;
|
||||
}
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
import { ROLE } from "../schema/index.js";
|
||||
|
||||
// Build OpenAI delta carrying reasoning_content (optional leading assistant role)
|
||||
export function reasoningDelta(text, withRole = false) {
|
||||
return withRole
|
||||
? { role: "assistant", reasoning_content: text }
|
||||
? { role: ROLE.ASSISTANT, reasoning_content: text }
|
||||
: { reasoning_content: text };
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Concern: reasoning_effort ↔ provider-native thinking config.
|
||||
// Each provider expresses "how much to think" differently — centralize the maps here.
|
||||
// Streaming reasoning delta shape lives in reasoning.js; this file is request-side config only.
|
||||
|
||||
// OpenAI reasoning_effort → Claude thinking.budget_tokens
|
||||
const EFFORT_TO_BUDGET = { none: 0, low: 4096, medium: 8192, high: 16384, xhigh: 32768 };
|
||||
|
||||
// Returns budget_tokens for a reasoning_effort, or undefined if unknown effort.
|
||||
// 0 means "no thinking" (caller skips enabling); undefined means "effort not recognized".
|
||||
export function effortToBudget(effort) {
|
||||
if (!effort) return undefined;
|
||||
return EFFORT_TO_BUDGET[String(effort).toLowerCase()];
|
||||
}
|
||||
|
||||
// OpenAI reasoning_effort → Gemini thinkingLevel (gemini-3 enum: minimal|low|medium|high).
|
||||
// Gemini 3 cannot fully disable thinking; "none"/"off" map to "minimal" (closest to off).
|
||||
export function effortToThinkingLevel(effort) {
|
||||
const e = String(effort).toLowerCase().trim();
|
||||
return e === "none" || e === "off" ? "minimal" : e;
|
||||
}
|
||||
|
||||
// Gemini thinkingBudget (numeric) → OpenAI reasoning_effort (antigravity reverse map).
|
||||
// Returns null when budget <= 0 (no reasoning).
|
||||
export function budgetToEffort(budget) {
|
||||
if (!budget || budget <= 0) return null;
|
||||
if (budget <= 2048) return "low";
|
||||
if (budget <= 16384) return "medium";
|
||||
return "high";
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Build OpenAI usage object. Caller computes prompt/completion/total (provider math).
|
||||
// Optional details added only when > 0 (matches existing claude/gemini/codex behavior).
|
||||
export function buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens = 0, cacheCreationTokens = 0, reasoningTokens = 0 }) {
|
||||
const usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: totalTokens };
|
||||
if (cachedTokens > 0 || cacheCreationTokens > 0) {
|
||||
usage.prompt_tokens_details = {};
|
||||
if (cachedTokens > 0) usage.prompt_tokens_details.cached_tokens = cachedTokens;
|
||||
if (cacheCreationTokens > 0) usage.prompt_tokens_details.cache_creation_tokens = cacheCreationTokens;
|
||||
}
|
||||
if (reasoningTokens > 0) {
|
||||
usage.completion_tokens_details = { reasoning_tokens: reasoningTokens };
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
const n = (v) => (typeof v === "number" ? v : 0);
|
||||
|
||||
// Per-provider raw token field-map + math. Returns buildUsage() args (NOT the usage object).
|
||||
// Keeps each provider's exact semantics: claude/gemini fold cache+reasoning, others don't.
|
||||
const USAGE_EXTRACTORS = {
|
||||
claude(raw) {
|
||||
const input = n(raw.input_tokens), output = n(raw.output_tokens);
|
||||
const cacheRead = n(raw.cache_read_input_tokens), cacheCreate = n(raw.cache_creation_input_tokens);
|
||||
const prompt = input + cacheRead + cacheCreate;
|
||||
return { promptTokens: prompt, completionTokens: output, totalTokens: prompt + output, cachedTokens: cacheRead, cacheCreationTokens: cacheCreate };
|
||||
},
|
||||
gemini(raw) {
|
||||
const cached = n(raw.cachedContentTokenCount);
|
||||
const prompt = n(raw.promptTokenCount);
|
||||
const thoughts = n(raw.thoughtsTokenCount);
|
||||
const total = n(raw.totalTokenCount);
|
||||
let candidates = n(raw.candidatesTokenCount);
|
||||
// Fallback: derive candidates from total when upstream omits it
|
||||
if (candidates === 0 && total > 0) {
|
||||
candidates = total - prompt - thoughts;
|
||||
if (candidates < 0) candidates = 0;
|
||||
}
|
||||
return { promptTokens: prompt, completionTokens: candidates + thoughts, totalTokens: total, cachedTokens: cached, reasoningTokens: thoughts };
|
||||
},
|
||||
kiro(raw) {
|
||||
const input = n(raw.inputTokens), output = n(raw.outputTokens);
|
||||
return { promptTokens: input, completionTokens: output, totalTokens: input + output };
|
||||
},
|
||||
ollama(raw) {
|
||||
const input = n(raw.prompt_eval_count), output = n(raw.eval_count);
|
||||
return { promptTokens: input, completionTokens: output, totalTokens: input + output };
|
||||
},
|
||||
commandcode(raw) {
|
||||
const input = n(raw.inputTokens), output = n(raw.outputTokens);
|
||||
const total = typeof raw.totalTokens === "number" ? raw.totalTokens : input + output;
|
||||
return { promptTokens: input, completionTokens: output, totalTokens: total };
|
||||
},
|
||||
};
|
||||
|
||||
// Convert provider-native usage object → OpenAI usage. Returns null if no extractor/raw.
|
||||
export function toOpenAIUsage(raw, kind) {
|
||||
const extract = USAGE_EXTRACTORS[kind];
|
||||
if (!extract || !raw || typeof raw !== "object") return null;
|
||||
return buildUsage(extract(raw));
|
||||
}
|
||||
+20
-19
@@ -1,6 +1,7 @@
|
||||
// Claude helper functions for translator
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import { adjustMaxTokens } from "./maxTokensHelper.js";
|
||||
import { ROLE, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import { adjustMaxTokens } from "./maxTokens.js";
|
||||
import { applyCloaking } from "../../utils/claudeCloaking.js";
|
||||
import { deriveSessionId } from "../../utils/sessionManager.js";
|
||||
import { PROVIDERS } from "../../providers/index.js";
|
||||
@@ -10,9 +11,9 @@ export function hasValidContent(msg) {
|
||||
if (typeof msg.content === "string" && msg.content.trim()) return true;
|
||||
if (Array.isArray(msg.content)) {
|
||||
return msg.content.some(block =>
|
||||
(block.type === "text" && block.text?.trim()) ||
|
||||
block.type === "tool_use" ||
|
||||
block.type === "tool_result"
|
||||
(block.type === CLAUDE_BLOCK.TEXT && block.text?.trim()) ||
|
||||
block.type === CLAUDE_BLOCK.TOOL_USE ||
|
||||
block.type === CLAUDE_BLOCK.TOOL_RESULT
|
||||
);
|
||||
}
|
||||
return false;
|
||||
@@ -26,18 +27,18 @@ export function fixToolUseOrdering(messages) {
|
||||
|
||||
// Pass 1: Fix assistant messages with tool_use - remove text after tool_use
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
const hasToolUse = msg.content.some(b => b.type === "tool_use");
|
||||
if (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) {
|
||||
const hasToolUse = msg.content.some(b => b.type === CLAUDE_BLOCK.TOOL_USE);
|
||||
if (hasToolUse) {
|
||||
// Keep only: thinking blocks + tool_use blocks (remove text blocks after tool_use)
|
||||
const newContent = [];
|
||||
let foundToolUse = false;
|
||||
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_use") {
|
||||
if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
foundToolUse = true;
|
||||
newContent.push(block);
|
||||
} else if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
} else if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) {
|
||||
newContent.push(block);
|
||||
} else if (!foundToolUse) {
|
||||
// Keep text blocks BEFORE tool_use
|
||||
@@ -59,17 +60,17 @@ export function fixToolUseOrdering(messages) {
|
||||
|
||||
if (last && last.role === msg.role) {
|
||||
// Merge content arrays
|
||||
const lastContent = Array.isArray(last.content) ? last.content : [{ type: "text", text: last.content }];
|
||||
const msgContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
|
||||
const lastContent = Array.isArray(last.content) ? last.content : [{ type: CLAUDE_BLOCK.TEXT, text: last.content }];
|
||||
const msgContent = Array.isArray(msg.content) ? msg.content : [{ type: CLAUDE_BLOCK.TEXT, text: msg.content }];
|
||||
|
||||
// Put tool_result first, then other content
|
||||
const toolResults = [...lastContent.filter(b => b.type === "tool_result"), ...msgContent.filter(b => b.type === "tool_result")];
|
||||
const otherContent = [...lastContent.filter(b => b.type !== "tool_result"), ...msgContent.filter(b => b.type !== "tool_result")];
|
||||
const toolResults = [...lastContent.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT), ...msgContent.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT)];
|
||||
const otherContent = [...lastContent.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT), ...msgContent.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT)];
|
||||
|
||||
last.content = [...toolResults, ...otherContent];
|
||||
} else {
|
||||
// Ensure content is array
|
||||
const content = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
|
||||
const content = Array.isArray(msg.content) ? msg.content : [{ type: CLAUDE_BLOCK.TEXT, text: msg.content }];
|
||||
merged.push({ role: msg.role, content: [...content] });
|
||||
}
|
||||
}
|
||||
@@ -97,13 +98,13 @@ export function normalizeClaudePassthrough(body, model = "") {
|
||||
const systemBlocks = [];
|
||||
const messages = [];
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "system") {
|
||||
if (msg.role === ROLE.SYSTEM) {
|
||||
const text = typeof msg.content === "string"
|
||||
? msg.content
|
||||
: Array.isArray(msg.content)
|
||||
? msg.content.map(b => (typeof b === "string" ? b : b?.text || "")).join("\n")
|
||||
: "";
|
||||
if (text.trim()) systemBlocks.push({ type: "text", text });
|
||||
if (text.trim()) systemBlocks.push({ type: CLAUDE_BLOCK.TEXT, text });
|
||||
continue;
|
||||
}
|
||||
messages.push(msg);
|
||||
@@ -191,7 +192,7 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
|
||||
if (!lastAssistantProcessed && msg.content.length > 0) {
|
||||
for (let j = msg.content.length - 1; j >= 0; j--) {
|
||||
const block = msg.content[j];
|
||||
if (block.type !== "thinking" && block.type !== "redacted_thinking") {
|
||||
if (block.type !== CLAUDE_BLOCK.THINKING && block.type !== CLAUDE_BLOCK.REDACTED_THINKING) {
|
||||
block.cache_control = { type: "ephemeral" };
|
||||
break;
|
||||
}
|
||||
@@ -206,17 +207,17 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
|
||||
|
||||
// Always replace signature for all thinking blocks
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) {
|
||||
block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE;
|
||||
hasThinking = true;
|
||||
}
|
||||
if (block.type === "tool_use") hasToolUse = true;
|
||||
if (block.type === CLAUDE_BLOCK.TOOL_USE) hasToolUse = true;
|
||||
}
|
||||
|
||||
// Add thinking block if thinking enabled + has tool_use but no thinking
|
||||
if (thinkingEnabled && !hasThinking && hasToolUse) {
|
||||
msg.content.unshift({
|
||||
type: "thinking",
|
||||
type: CLAUDE_BLOCK.THINKING,
|
||||
thinking: ".",
|
||||
signature: DEFAULT_THINKING_CLAUDE_SIGNATURE
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// Gemini helper functions for translator
|
||||
|
||||
import { safeParseJSON } from "./jsonUtil.js";
|
||||
import { safeParseJSON } from "../concerns/json.js";
|
||||
import { OPENAI_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Unsupported JSON Schema constraints that should be removed for Antigravity
|
||||
export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
|
||||
@@ -41,9 +42,9 @@ export function convertOpenAIContentToParts(content) {
|
||||
parts.push({ text: content });
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const item of content) {
|
||||
if (item.type === "text") {
|
||||
if (item.type === OPENAI_BLOCK.TEXT) {
|
||||
parts.push({ text: item.text });
|
||||
} else if (item.type === "image_url" && item.image_url?.url?.startsWith("data:")) {
|
||||
} else if (item.type === OPENAI_BLOCK.IMAGE_URL && item.image_url?.url?.startsWith("data:")) {
|
||||
const url = item.image_url.url;
|
||||
const commaIndex = url.indexOf(",");
|
||||
if (commaIndex !== -1) {
|
||||
@@ -55,17 +56,17 @@ export function convertOpenAIContentToParts(content) {
|
||||
inlineData: { mime_type: mimeType, data: data }
|
||||
});
|
||||
}
|
||||
} else if (item.type === "image_url" && item.image_url?.url && (item.image_url.url.startsWith("http://") || item.image_url.url.startsWith("https://"))) {
|
||||
} else if (item.type === OPENAI_BLOCK.IMAGE_URL && item.image_url?.url && (item.image_url.url.startsWith("http://") || item.image_url.url.startsWith("https://"))) {
|
||||
parts.push({
|
||||
fileData: { fileUri: item.image_url.url, mimeType: "image/*" }
|
||||
});
|
||||
} else if (item.type === "input_audio" && item.input_audio?.data) {
|
||||
} else if (item.type === OPENAI_BLOCK.INPUT_AUDIO && item.input_audio?.data) {
|
||||
const format = item.input_audio.format || "wav";
|
||||
const mimeType = format === "mp3" ? "audio/mpeg" : `audio/${format}`;
|
||||
parts.push({
|
||||
inlineData: { mime_type: mimeType, data: item.input_audio.data }
|
||||
});
|
||||
} else if (item.type === "audio_url" && item.audio_url?.url?.startsWith("data:")) {
|
||||
} else if (item.type === OPENAI_BLOCK.AUDIO_URL && item.audio_url?.url?.startsWith("data:")) {
|
||||
const url = item.audio_url.url;
|
||||
const commaIndex = url.indexOf(",");
|
||||
if (commaIndex !== -1) {
|
||||
@@ -84,10 +85,10 @@ export function convertOpenAIContentToParts(content) {
|
||||
}
|
||||
|
||||
// Extract text content from OpenAI content
|
||||
export function extractTextContent(content) {
|
||||
export function extractTextContent(content, separator = "") {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content.filter(c => c.type === "text").map(c => c.text).join("");
|
||||
return content.filter(c => c.type === OPENAI_BLOCK.TEXT).map(c => c.text).join(separator);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
+18
-18
@@ -1,8 +1,8 @@
|
||||
// OpenAI helper functions for translator
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK, VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES } from "../schema/index.js";
|
||||
|
||||
// Valid OpenAI content block types
|
||||
export const VALID_OPENAI_CONTENT_TYPES = ["text", "image_url", "image", "input_audio", "audio_url"];
|
||||
export const VALID_OPENAI_MESSAGE_TYPES = ["text", "image_url", "image", "tool_calls", "tool_result"];
|
||||
// Re-export valid-type lists (moved to schema/blocks.js) to keep existing importers working.
|
||||
export { VALID_OPENAI_CONTENT_TYPES, VALID_OPENAI_MESSAGE_TYPES };
|
||||
|
||||
// Filter messages to OpenAI standard format
|
||||
// Remove: thinking, redacted_thinking, signature, and other non-OpenAI blocks
|
||||
@@ -11,13 +11,13 @@ export function filterToOpenAIFormat(body) {
|
||||
|
||||
body.messages = body.messages.map(msg => {
|
||||
// Normalize developer role to system (many providers don't support developer)
|
||||
if (msg.role === "developer") msg = { ...msg, role: "system" };
|
||||
if (msg.role === ROLE.DEVELOPER) msg = { ...msg, role: ROLE.SYSTEM };
|
||||
|
||||
// Keep tool messages as-is (OpenAI format)
|
||||
if (msg.role === "tool") return msg;
|
||||
if (msg.role === ROLE.TOOL) return msg;
|
||||
|
||||
// Keep assistant messages with tool_calls as-is
|
||||
if (msg.role === "assistant" && msg.tool_calls) return msg;
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return msg;
|
||||
|
||||
// Handle string content
|
||||
if (typeof msg.content === "string") return msg;
|
||||
@@ -28,17 +28,17 @@ export function filterToOpenAIFormat(body) {
|
||||
|
||||
for (const block of msg.content) {
|
||||
// Skip thinking blocks
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") continue;
|
||||
if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) continue;
|
||||
|
||||
// Only keep valid OpenAI content types
|
||||
if (VALID_OPENAI_CONTENT_TYPES.includes(block.type)) {
|
||||
// Remove signature field if exists
|
||||
const { signature, cache_control, ...cleanBlock } = block;
|
||||
filteredContent.push(cleanBlock);
|
||||
} else if (block.type === "tool_use") {
|
||||
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||
// Convert tool_use to tool_calls format (handled separately)
|
||||
continue;
|
||||
} else if (block.type === "tool_result") {
|
||||
} else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
|
||||
// Keep tool_result but clean it
|
||||
const { signature, cache_control, ...cleanBlock } = block;
|
||||
filteredContent.push(cleanBlock);
|
||||
@@ -47,7 +47,7 @@ export function filterToOpenAIFormat(body) {
|
||||
|
||||
// If all content was filtered, add empty text
|
||||
if (filteredContent.length === 0) {
|
||||
filteredContent.push({ type: "text", text: "" });
|
||||
filteredContent.push({ type: OPENAI_BLOCK.TEXT, text: "" });
|
||||
}
|
||||
|
||||
return { ...msg, content: filteredContent };
|
||||
@@ -59,15 +59,15 @@ export function filterToOpenAIFormat(body) {
|
||||
// Filter out messages with only empty text (but NEVER filter tool messages)
|
||||
body.messages = body.messages.filter(msg => {
|
||||
// Always keep tool messages
|
||||
if (msg.role === "tool") return true;
|
||||
if (msg.role === ROLE.TOOL) return true;
|
||||
// Always keep assistant messages with tool_calls
|
||||
if (msg.role === "assistant" && msg.tool_calls) return true;
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) return true;
|
||||
|
||||
if (typeof msg.content === "string") return msg.content.trim() !== "";
|
||||
if (Array.isArray(msg.content)) {
|
||||
return msg.content.some(b =>
|
||||
(b.type === "text" && b.text?.trim()) ||
|
||||
b.type !== "text"
|
||||
(b.type === OPENAI_BLOCK.TEXT && b.text?.trim()) ||
|
||||
b.type !== OPENAI_BLOCK.TEXT
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -82,12 +82,12 @@ export function filterToOpenAIFormat(body) {
|
||||
if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) {
|
||||
body.tools = body.tools.map(tool => {
|
||||
// Already OpenAI format
|
||||
if (tool.type === "function" && tool.function) return tool;
|
||||
if (tool.type === OPENAI_BLOCK.FUNCTION && tool.function) return tool;
|
||||
|
||||
// Claude format: {name, description, input_schema}
|
||||
if (tool.name && (tool.input_schema || tool.description)) {
|
||||
return {
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: String(tool.description || ""),
|
||||
@@ -99,7 +99,7 @@ export function filterToOpenAIFormat(body) {
|
||||
// Gemini format: {functionDeclarations: [{name, description, parameters}]}
|
||||
if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) {
|
||||
return tool.functionDeclarations.map(fn => ({
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: fn.name,
|
||||
description: String(fn.description || ""),
|
||||
@@ -121,7 +121,7 @@ export function filterToOpenAIFormat(body) {
|
||||
} else if (choice.type === "any") {
|
||||
body.tool_choice = "required";
|
||||
} else if (choice.type === "tool" && choice.name) {
|
||||
body.tool_choice = { type: "function", function: { name: choice.name } };
|
||||
body.tool_choice = { type: OPENAI_BLOCK.FUNCTION, function: { name: choice.name } };
|
||||
}
|
||||
}
|
||||
|
||||
+17
-15
@@ -1,3 +1,5 @@
|
||||
import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM } from "../schema/index.js";
|
||||
|
||||
/**
|
||||
* Normalize Responses API input to array format.
|
||||
* Accepts string or array, returns array of message items.
|
||||
@@ -9,12 +11,12 @@
|
||||
export function normalizeResponsesInput(input) {
|
||||
if (typeof input === "string") {
|
||||
const text = input.trim() === "" ? "..." : input;
|
||||
return [{ type: "message", role: "user", content: [{ type: "input_text", text }] }];
|
||||
return [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text }] }];
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
// Empty input[] would produce messages:[] which all providers reject (#389)
|
||||
if (input.length === 0) {
|
||||
return [{ type: "message", role: "user", content: [{ type: "input_text", text: "..." }] }];
|
||||
return [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "..." }] }];
|
||||
}
|
||||
return input;
|
||||
}
|
||||
@@ -34,7 +36,7 @@ export function convertResponsesApiFormat(body) {
|
||||
|
||||
// Convert instructions to system message
|
||||
if (body.instructions) {
|
||||
result.messages.push({ role: "system", content: body.instructions });
|
||||
result.messages.push({ role: ROLE.SYSTEM, content: body.instructions });
|
||||
}
|
||||
|
||||
// Group items by conversation turn
|
||||
@@ -48,9 +50,9 @@ export function convertResponsesApiFormat(body) {
|
||||
for (const item of inputItems) {
|
||||
// Determine item type - Droid CLI sends role-based items without 'type' field
|
||||
// Fallback: if no type but has role property, treat as message
|
||||
const itemType = item.type || (item.role ? "message" : null);
|
||||
const itemType = item.type || (item.role ? RESPONSES_ITEM.MESSAGE : null);
|
||||
|
||||
if (itemType === "message") {
|
||||
if (itemType === RESPONSES_ITEM.MESSAGE) {
|
||||
// Flush any pending assistant message with tool calls
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
@@ -67,22 +69,22 @@ export function convertResponsesApiFormat(body) {
|
||||
// Convert content: input_text → text, output_text → text, input_image → image_url
|
||||
const content = Array.isArray(item.content)
|
||||
? item.content.map(c => {
|
||||
if (c.type === "input_text") return { type: "text", text: c.text };
|
||||
if (c.type === "output_text") return { type: "text", text: c.text };
|
||||
if (c.type === "input_image") {
|
||||
if (c.type === RESPONSES_ITEM.INPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text };
|
||||
if (c.type === RESPONSES_ITEM.OUTPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text };
|
||||
if (c.type === RESPONSES_ITEM.INPUT_IMAGE) {
|
||||
const url = c.image_url || c.file_id || "";
|
||||
return { type: "image_url", image_url: { url, detail: c.detail || "auto" } };
|
||||
return { type: OPENAI_BLOCK.IMAGE_URL, image_url: { url, detail: c.detail || "auto" } };
|
||||
}
|
||||
return c;
|
||||
})
|
||||
: item.content;
|
||||
result.messages.push({ role: item.role, content });
|
||||
}
|
||||
else if (itemType === "function_call") {
|
||||
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) {
|
||||
// Start or append to assistant message with tool_calls
|
||||
if (!currentAssistantMsg) {
|
||||
currentAssistantMsg = {
|
||||
role: "assistant",
|
||||
role: ROLE.ASSISTANT,
|
||||
content: null,
|
||||
tool_calls: []
|
||||
};
|
||||
@@ -91,14 +93,14 @@ export function convertResponsesApiFormat(body) {
|
||||
if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue;
|
||||
currentAssistantMsg.tool_calls.push({
|
||||
id: item.call_id,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: item.name,
|
||||
arguments: item.arguments
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (itemType === "function_call_output") {
|
||||
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL_OUTPUT) {
|
||||
// Flush assistant message first if exists
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
@@ -106,12 +108,12 @@ export function convertResponsesApiFormat(body) {
|
||||
}
|
||||
// Add tool result
|
||||
pendingToolResults.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: item.call_id,
|
||||
content: typeof item.output === "string" ? item.output : JSON.stringify(item.output)
|
||||
});
|
||||
}
|
||||
else if (itemType === "reasoning") {
|
||||
else if (itemType === RESPONSES_ITEM.REASONING) {
|
||||
// Skip reasoning items - they are for display only
|
||||
continue;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Build OpenAI usage object. Caller computes prompt/completion/total (provider math).
|
||||
// Optional details added only when > 0 (matches existing claude/gemini/codex behavior).
|
||||
export function buildUsage({ promptTokens, completionTokens, totalTokens, cachedTokens = 0, cacheCreationTokens = 0, reasoningTokens = 0 }) {
|
||||
const usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: totalTokens };
|
||||
if (cachedTokens > 0 || cacheCreationTokens > 0) {
|
||||
usage.prompt_tokens_details = {};
|
||||
if (cachedTokens > 0) usage.prompt_tokens_details.cached_tokens = cachedTokens;
|
||||
if (cacheCreationTokens > 0) usage.prompt_tokens_details.cache_creation_tokens = cacheCreationTokens;
|
||||
}
|
||||
if (reasoningTokens > 0) {
|
||||
usage.completion_tokens_details = { reasoning_tokens: reasoningTokens };
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { FORMATS } from "./formats.js";
|
||||
import { ensureToolCallIds, fixMissingToolResponses } from "./helpers/toolCallHelper.js";
|
||||
import { prepareClaudeRequest } from "./helpers/claudeHelper.js";
|
||||
import { ensureToolCallIds, fixMissingToolResponses } from "./concerns/toolCall.js";
|
||||
import { prepareClaudeRequest } from "./formats/claude.js";
|
||||
import { cloakClaudeTools } from "../utils/claudeCloaking.js";
|
||||
import { filterToOpenAIFormat } from "./helpers/openaiHelper.js";
|
||||
import { filterToOpenAIFormat } from "./formats/openai.js";
|
||||
import { normalizeThinkingConfig } from "../services/provider.js";
|
||||
import { AntigravityExecutor } from "../executors/antigravity.js";
|
||||
import { PROVIDERS } from "../providers/index.js";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { encodeDataUri } from "../helpers/imageHelper.js";
|
||||
import { adjustMaxTokens } from "../formats/maxTokens.js";
|
||||
import { encodeDataUri } from "../concerns/image.js";
|
||||
import { ROLE, GEMINI_ROLE, OPENAI_BLOCK } from "../schema/index.js";
|
||||
import { budgetToEffort } from "../concerns/thinking.js";
|
||||
import { collapseTextParts } from "../concerns/message.js";
|
||||
|
||||
// Convert Antigravity request to OpenAI format
|
||||
// Antigravity body: { project, model, userAgent, requestType, requestId, request: { contents, systemInstruction, tools, toolConfig, generationConfig, sessionId } }
|
||||
@@ -32,16 +35,8 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
|
||||
// Thinking config → reasoning_effort
|
||||
if (config.thinkingConfig) {
|
||||
const budget = config.thinkingConfig.thinkingBudget || 0;
|
||||
if (budget > 0) {
|
||||
if (budget <= 2048) {
|
||||
result.reasoning_effort = "low";
|
||||
} else if (budget <= 16384) {
|
||||
result.reasoning_effort = "medium";
|
||||
} else {
|
||||
result.reasoning_effort = "high";
|
||||
}
|
||||
}
|
||||
const effort = budgetToEffort(config.thinkingConfig.thinkingBudget || 0);
|
||||
if (effort) result.reasoning_effort = effort;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +44,7 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
if (req.systemInstruction) {
|
||||
const systemText = extractText(req.systemInstruction);
|
||||
if (systemText) {
|
||||
result.messages.push({ role: "system", content: systemText });
|
||||
result.messages.push({ role: ROLE.SYSTEM, content: systemText });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +69,7 @@ export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
if (tool.functionDeclarations) {
|
||||
for (const func of tool.functionDeclarations) {
|
||||
result.tools.push({
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: func.name,
|
||||
description: func.description || "",
|
||||
@@ -123,7 +118,7 @@ function normalizeSchemaTypes(schema) {
|
||||
// Convert Antigravity content to OpenAI message
|
||||
// Handles: text, thought, thoughtSignature, functionCall, functionResponse, inlineData
|
||||
function convertContent(content) {
|
||||
const role = content.role === "model" ? "assistant" : content.role === "user" ? "user" : content.role;
|
||||
const role = content.role === GEMINI_ROLE.MODEL ? ROLE.ASSISTANT : content.role === GEMINI_ROLE.USER ? ROLE.USER : content.role;
|
||||
|
||||
if (!content.parts || !Array.isArray(content.parts)) {
|
||||
return null;
|
||||
@@ -143,19 +138,19 @@ function convertContent(content) {
|
||||
|
||||
// Text with thoughtSignature = regular text after thinking
|
||||
if (part.thoughtSignature && part.text !== undefined) {
|
||||
textParts.push({ type: "text", text: part.text });
|
||||
textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular text
|
||||
if (part.text !== undefined) {
|
||||
textParts.push({ type: "text", text: part.text });
|
||||
textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
|
||||
}
|
||||
|
||||
// Inline data (images)
|
||||
if (part.inlineData) {
|
||||
textParts.push({
|
||||
type: "image_url",
|
||||
type: OPENAI_BLOCK.IMAGE_URL,
|
||||
image_url: {
|
||||
url: encodeDataUri(part.inlineData.mimeType, part.inlineData.data)
|
||||
}
|
||||
@@ -166,7 +161,7 @@ function convertContent(content) {
|
||||
if (part.functionCall) {
|
||||
toolCalls.push({
|
||||
id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: part.functionCall.name,
|
||||
arguments: JSON.stringify(part.functionCall.args || {})
|
||||
@@ -177,7 +172,7 @@ function convertContent(content) {
|
||||
// Function response → collect all, each becomes a separate tool message
|
||||
if (part.functionResponse) {
|
||||
toolResults.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: part.functionResponse.id || part.functionResponse.name,
|
||||
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
|
||||
});
|
||||
@@ -191,9 +186,9 @@ function convertContent(content) {
|
||||
|
||||
// Assistant with tool calls
|
||||
if (toolCalls.length > 0) {
|
||||
const msg = { role: "assistant" };
|
||||
const msg = { role: ROLE.ASSISTANT };
|
||||
if (textParts.length > 0) {
|
||||
msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
|
||||
msg.content = collapseTextParts(textParts);
|
||||
}
|
||||
if (reasoningContent) {
|
||||
msg.reasoning_content = reasoningContent;
|
||||
@@ -206,7 +201,7 @@ function convertContent(content) {
|
||||
if (textParts.length > 0 || reasoningContent) {
|
||||
const msg = { role };
|
||||
if (textParts.length > 0) {
|
||||
msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
|
||||
msg.content = collapseTextParts(textParts);
|
||||
}
|
||||
if (reasoningContent) {
|
||||
msg.reasoning_content = reasoningContent;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { encodeDataUri } from "../helpers/imageHelper.js";
|
||||
import { adjustMaxTokens } from "../formats/maxTokens.js";
|
||||
import { encodeDataUri } from "../concerns/image.js";
|
||||
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import { collapseTextParts } from "../concerns/message.js";
|
||||
|
||||
function stripAnthropicBillingHeader(text) {
|
||||
if (typeof text !== "string") return "";
|
||||
@@ -34,7 +36,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
|
||||
if (systemContent) {
|
||||
result.messages.push({
|
||||
role: "system",
|
||||
role: ROLE.SYSTEM,
|
||||
content: systemContent
|
||||
});
|
||||
}
|
||||
@@ -58,13 +60,13 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
|
||||
// Fix missing tool responses - OpenAI requires every tool_call to have a response.
|
||||
// Local variant: scans contiguous tool replies + inserts "[No response received]"
|
||||
// (distinct from the global immediate-next check in toolCallHelper, runs on the openai leg).
|
||||
// (distinct from the global immediate-next check in concerns/toolCall, runs on the openai leg).
|
||||
fixMissingToolResponsesOpenAI(result.messages);
|
||||
|
||||
// Tools
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
result.tools = body.tools.map(tool => ({
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: String(tool.description || ""),
|
||||
@@ -85,7 +87,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
function fixMissingToolResponsesOpenAI(messages) {
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
if (msg.role === ROLE.ASSISTANT && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
const toolCallIds = msg.tool_calls.map(tc => tc.id);
|
||||
|
||||
// Collect all tool response IDs that IMMEDIATELY follow this assistant message
|
||||
@@ -93,7 +95,7 @@ function fixMissingToolResponsesOpenAI(messages) {
|
||||
let insertPosition = i + 1;
|
||||
for (let j = i + 1; j < messages.length; j++) {
|
||||
const nextMsg = messages[j];
|
||||
if (nextMsg.role === "tool" && nextMsg.tool_call_id) {
|
||||
if (nextMsg.role === ROLE.TOOL && nextMsg.tool_call_id) {
|
||||
respondedIds.add(nextMsg.tool_call_id);
|
||||
insertPosition = j + 1;
|
||||
} else {
|
||||
@@ -106,7 +108,7 @@ function fixMissingToolResponsesOpenAI(messages) {
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
const missingResponses = missingIds.map(id => ({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: id,
|
||||
content: "[No response received]"
|
||||
}));
|
||||
@@ -119,7 +121,7 @@ function fixMissingToolResponsesOpenAI(messages) {
|
||||
|
||||
// Convert single Claude message - returns single message or array of messages
|
||||
function convertClaudeMessage(msg) {
|
||||
const role = msg.role === "user" || msg.role === "tool" ? "user" : "assistant";
|
||||
const role = msg.role === ROLE.USER || msg.role === ROLE.TOOL ? ROLE.USER : ROLE.ASSISTANT;
|
||||
|
||||
// Simple string content
|
||||
if (typeof msg.content === "string") {
|
||||
@@ -134,14 +136,14 @@ function convertClaudeMessage(msg) {
|
||||
|
||||
for (const block of msg.content) {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
parts.push({ type: "text", text: block.text });
|
||||
case CLAUDE_BLOCK.TEXT:
|
||||
parts.push({ type: OPENAI_BLOCK.TEXT, text: block.text });
|
||||
break;
|
||||
|
||||
case "image":
|
||||
case CLAUDE_BLOCK.IMAGE:
|
||||
if (block.source?.type === "base64") {
|
||||
parts.push({
|
||||
type: "image_url",
|
||||
type: OPENAI_BLOCK.IMAGE_URL,
|
||||
image_url: {
|
||||
url: encodeDataUri(block.source.media_type, block.source.data)
|
||||
}
|
||||
@@ -149,10 +151,10 @@ function convertClaudeMessage(msg) {
|
||||
}
|
||||
break;
|
||||
|
||||
case "tool_use":
|
||||
case CLAUDE_BLOCK.TOOL_USE:
|
||||
toolCalls.push({
|
||||
id: block.id,
|
||||
type: "function",
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: block.name,
|
||||
arguments: JSON.stringify(block.input || {})
|
||||
@@ -160,13 +162,13 @@ function convertClaudeMessage(msg) {
|
||||
});
|
||||
break;
|
||||
|
||||
case "tool_result":
|
||||
case CLAUDE_BLOCK.TOOL_RESULT:
|
||||
let resultContent = "";
|
||||
if (typeof block.content === "string") {
|
||||
resultContent = block.content;
|
||||
} else if (Array.isArray(block.content)) {
|
||||
resultContent = block.content
|
||||
.filter(c => c.type === "text")
|
||||
.filter(c => c.type === CLAUDE_BLOCK.TEXT)
|
||||
.map(c => c.text)
|
||||
.join("\n") || JSON.stringify(block.content);
|
||||
} else if (block.content) {
|
||||
@@ -174,7 +176,7 @@ function convertClaudeMessage(msg) {
|
||||
}
|
||||
|
||||
toolResults.push({
|
||||
role: "tool",
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: block.tool_use_id,
|
||||
content: resultContent
|
||||
});
|
||||
@@ -185,21 +187,16 @@ function convertClaudeMessage(msg) {
|
||||
// If has tool results, return array of tool messages
|
||||
if (toolResults.length > 0) {
|
||||
if (parts.length > 0) {
|
||||
const textContent = parts.length === 1 && parts[0].type === "text"
|
||||
? parts[0].text
|
||||
: parts;
|
||||
return [...toolResults, { role: "user", content: textContent }];
|
||||
return [...toolResults, { role: ROLE.USER, content: collapseTextParts(parts) }];
|
||||
}
|
||||
return toolResults;
|
||||
}
|
||||
|
||||
// If has tool calls, return assistant message with tool_calls
|
||||
if (toolCalls.length > 0) {
|
||||
const result = { role: "assistant" };
|
||||
const result = { role: ROLE.ASSISTANT };
|
||||
if (parts.length > 0) {
|
||||
result.content = parts.length === 1 && parts[0].type === "text"
|
||||
? parts[0].text
|
||||
: parts;
|
||||
result.content = collapseTextParts(parts);
|
||||
}
|
||||
result.tool_calls = toolCalls;
|
||||
return result;
|
||||
@@ -209,7 +206,7 @@ function convertClaudeMessage(msg) {
|
||||
if (parts.length > 0) {
|
||||
return {
|
||||
role,
|
||||
content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts
|
||||
content: collapseTextParts(parts)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -230,7 +227,7 @@ function convertToolChoice(choice) {
|
||||
switch (choice.type) {
|
||||
case "auto": return "auto";
|
||||
case "any": return "required";
|
||||
case "tool": return { type: "function", function: { name: choice.name } };
|
||||
case "tool": return { type: OPENAI_BLOCK.FUNCTION, function: { name: choice.name } };
|
||||
default: return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user