diff --git a/.env.example b/.env.example index 8f9c917..10975c7 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ # ============================================================================== # Primary model provider to use -# Options: databricks, azure-anthropic, azure-openai, openrouter, openai, ollama, llamacpp, lmstudio, bedrock, zai, vertex +# Options: databricks, azure-anthropic, azure-openai, openrouter, openai, ollama, llamacpp, lmstudio, bedrock, zai, moonshot, vertex # Default: databricks MODEL_PROVIDER=ollama @@ -158,6 +158,14 @@ OLLAMA_MAX_TOOLS_FOR_ROUTING=3 # Options: GLM-4.7, GLM-4.5-Air, GLM-4-Plus # ZAI_MODEL=GLM-4.7 +# ============================================================================== +# Moonshot (Kimi) Configuration (Anthropic-compatible endpoint) +# ============================================================================== + +# MOONSHOT_API_KEY=your-moonshot-api-key +# MOONSHOT_ENDPOINT=https://api.moonshot.ai/anthropic/v1/messages +# MOONSHOT_MODEL=kimi-k2.5 + # ============================================================================== # Google Vertex AI Configuration (Gemini Models) # ============================================================================== @@ -361,6 +369,12 @@ HOT_RELOAD_DEBOUNCE_MS=1000 # ZAI_MODEL=GLM-4.7 # npm start +# Moonshot (Kimi): +# MODEL_PROVIDER=moonshot +# MOONSHOT_API_KEY=your-moonshot-api-key +# MOONSHOT_MODEL=kimi-k2.5 +# npm start + # Google Gemini (via Vertex AI): # MODEL_PROVIDER=vertex # VERTEX_API_KEY=your-google-api-key diff --git a/src/api/openai-router.js b/src/api/openai-router.js index ab283e7..86d2239 100644 --- a/src/api/openai-router.js +++ b/src/api/openai-router.js @@ -767,6 +767,18 @@ function getConfiguredProviders() { }); } + // Check Moonshot (Kimi) + if (config.moonshot?.apiKey) { + providers.push({ + name: "moonshot", + type: "moonshot-ai", + models: [ + config.moonshot.model || "kimi-k2.5", + "kimi-k2.5" + ] + }); + } + // Check Vertex AI (Google Cloud) if (config.vertex?.projectId) { providers.push({ diff --git a/src/api/providers-handler.js b/src/api/providers-handler.js index 9b85848..c6bb4fa 100644 --- a/src/api/providers-handler.js +++ b/src/api/providers-handler.js @@ -179,6 +179,20 @@ function getConfiguredProviders() { }); } + // Check Moonshot (Kimi) + if (config.moonshot?.apiKey) { + providers.push({ + name: "moonshot", + type: "moonshot-ai", + baseUrl: config.moonshot.endpoint || "https://api.moonshot.ai/api/anthropic", + enabled: true, + models: [ + { id: config.moonshot.model || "kimi-k2.5", name: "Configured Model" }, + { id: "kimi-k2.5", name: "Kimi K2.5" }, + ] + }); + } + // Check Vertex AI (Google Cloud) if (config.vertex?.projectId) { const region = config.vertex.region || "us-east5"; diff --git a/src/clients/databricks.js b/src/clients/databricks.js index 9b536cd..f71d078 100644 --- a/src/clients/databricks.js +++ b/src/clients/databricks.js @@ -1261,16 +1261,21 @@ async function invokeBedrock(body) { * Z.AI offers GLM models through an Anthropic-compatible API at ~1/7 the cost. * Minimal transformation needed - mostly passthrough with model mapping. */ -async function invokeZai(body) { - if (!config.zai?.apiKey) { - throw new Error("Z.AI API key is not configured. Set ZAI_API_KEY in your .env file."); +async function invokeZai(body, providerOptions = {}) { + const providerConfig = providerOptions.config || config.zai || {}; + const providerName = providerOptions.providerName || "Z.AI"; + const defaultEndpoint = providerOptions.defaultEndpoint || "https://api.z.ai/api/anthropic/v1/messages"; + const defaultModel = providerOptions.defaultModel || "glm-4.7"; + + if (!providerConfig.apiKey) { + throw new Error(`${providerName} API key is not configured.`); } - const endpoint = config.zai.endpoint || "https://api.z.ai/api/anthropic/v1/messages"; + const endpoint = providerConfig.endpoint || defaultEndpoint; const isOpenAIFormat = endpoint.includes("/chat/completions"); // Model mapping: Anthropic names → Z.AI names (lowercase) - const modelMap = { + const modelMap = providerOptions.modelMap || { "claude-sonnet-4-5-20250929": "glm-4.7", "claude-sonnet-4-5": "glm-4.7", "claude-sonnet-4.5": "glm-4.7", @@ -1280,8 +1285,9 @@ async function invokeZai(body) { "claude-3-haiku": "glm-4.5-air", }; - const requestedModel = body.model || config.zai.model; - let mappedModel = modelMap[requestedModel] || config.zai.model || "glm-4.7"; + const requestedModel = body.model || providerConfig.model; + // If operator explicitly sets provider model, honor it over Claude-name mapping. + let mappedModel = providerConfig.model || modelMap[requestedModel] || defaultModel; mappedModel = mappedModel.toLowerCase(); let zaiBody; @@ -1362,26 +1368,42 @@ async function invokeZai(body) { headers = { "Content-Type": "application/json", - "Authorization": `Bearer ${config.zai.apiKey}`, + "Authorization": `Bearer ${providerConfig.apiKey}`, }; } else { // Anthropic format endpoint zaiBody = { ...body }; zaiBody.model = mappedModel; + const hasToolHistory = Array.isArray(zaiBody.messages) + && zaiBody.messages.some((message) => { + if (!message || !Array.isArray(message.content)) return false; + return message.content.some((block) => ( + block?.type === "tool_use" + || block?.type === "tool_result" + || block?.type === "tool_reference" + )); + }); + // Inject standard tools if client didn't send any (passthrough mode) - if (!Array.isArray(zaiBody.tools) || zaiBody.tools.length === 0) { + // IMPORTANT: do not inject on tool-followup turns, because the model + // must continue against the exact previously-declared tool schema. + if ((!Array.isArray(zaiBody.tools) || zaiBody.tools.length === 0) && !hasToolHistory) { zaiBody.tools = STANDARD_TOOLS; logger.info({ injectedToolCount: STANDARD_TOOLS.length, injectedToolNames: STANDARD_TOOLS.map(t => t.name), reason: "Client did not send tools (passthrough mode)" - }, "=== INJECTING STANDARD TOOLS (Z.AI Anthropic) ==="); + }, `=== INJECTING STANDARD TOOLS (${providerName} Anthropic) ===`); + } else if ((!Array.isArray(zaiBody.tools) || zaiBody.tools.length === 0) && hasToolHistory) { + logger.info({ + reason: "Skipped tool injection on tool-followup turn", + }, `=== TOOL INJECTION SKIPPED (${providerName} Anthropic) ===`); } headers = { "Content-Type": "application/json", - "x-api-key": config.zai.apiKey, + "x-api-key": providerConfig.apiKey, "anthropic-version": "2023-06-01", }; } @@ -1401,20 +1423,20 @@ async function invokeZai(body) { toolNames: zaiBody.tools?.map(t => t.function?.name || t.name), toolChoice: zaiBody.tool_choice, fullRequest: JSON.stringify(zaiBody).substring(0, 500), - }, "=== Z.AI REQUEST ==="); + }, `=== ${providerName} REQUEST ===`); logger.debug({ zaiBody: JSON.stringify(zaiBody).substring(0, 1000), - }, "Z.AI request body (truncated)"); + }, `${providerName} request body (truncated)`); - // Use semaphore to limit concurrent Z.AI requests (prevents rate limiting) + // Use semaphore to limit concurrent requests (prevents rate limiting) return zaiSemaphore.run(async () => { logger.debug({ queueLength: zaiSemaphore.queue.length, currentConcurrent: zaiSemaphore.current, - }, "Z.AI semaphore status"); + }, `${providerName} semaphore status`); - const response = await performJsonRequest(endpoint, { headers, body: zaiBody }, "Z.AI"); + const response = await performJsonRequest(endpoint, { headers, body: zaiBody }, providerName); logger.info({ responseOk: response?.ok, @@ -1423,14 +1445,14 @@ async function invokeZai(body) { rawContent: response?.json?.choices?.[0]?.message?.content, hasReasoning: !!response?.json?.choices?.[0]?.message?.reasoning_content, isOpenAIFormat, - }, "=== Z.AI RAW RESPONSE ==="); + }, `=== ${providerName} RAW RESPONSE ===`); // Convert OpenAI response back to Anthropic format if needed if (isOpenAIFormat && response?.ok && response?.json) { const anthropicJson = convertOpenAIToAnthropic(response.json); logger.info({ convertedContent: JSON.stringify(anthropicJson.content).substring(0, 200), - }, "=== Z.AI CONVERTED RESPONSE ==="); + }, `=== ${providerName} CONVERTED RESPONSE ===`); // Return in the same format as other providers (with ok, status, json) return { ok: response.ok, @@ -1446,6 +1468,26 @@ async function invokeZai(body) { }); } +async function invokeMoonshot(body) { + const moonshotModelMap = { + "claude-sonnet-4-5-20250929": "kimi-k2.5", + "claude-sonnet-4-5": "kimi-k2.5", + "claude-sonnet-4.5": "kimi-k2.5", + "claude-3-5-sonnet": "kimi-k2.5", + "claude-haiku-4-5-20251001": "kimi-k2.5", + "claude-haiku-4-5": "kimi-k2.5", + "claude-3-haiku": "kimi-k2.5", + }; + + return invokeZai(body, { + providerName: "Moonshot", + config: config.moonshot, + defaultEndpoint: "https://api.moonshot.ai/anthropic/v1/messages", + defaultModel: "kimi-k2.5", + modelMap: moonshotModelMap, + }); +} + /** @@ -1883,6 +1925,8 @@ async function invokeModel(body, options = {}) { return await invokeBedrock(body); } else if (initialProvider === "zai") { return await invokeZai(body); + } else if (initialProvider === "moonshot") { + return await invokeMoonshot(body); } else if (initialProvider === "vertex") { return await invokeVertex(body); } @@ -1972,6 +2016,8 @@ async function invokeModel(body, options = {}) { return await invokeLlamaCpp(body); } else if (fallbackProvider === "zai") { return await invokeZai(body); + } else if (fallbackProvider === "moonshot") { + return await invokeMoonshot(body); } else if (fallbackProvider === "vertex") { return await invokeVertex(body); } diff --git a/src/clients/ollama-utils.js b/src/clients/ollama-utils.js index 7582f05..1815c8f 100644 --- a/src/clients/ollama-utils.js +++ b/src/clients/ollama-utils.js @@ -11,6 +11,7 @@ const TOOL_CAPABLE_MODELS = new Set([ "llama3.1", "llama3.2", "qwen2.5", + "qwen3", "mistral", "mistral-nemo", "firefunction-v2", diff --git a/src/config/index.js b/src/config/index.js index 466585d..42ee558 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -62,7 +62,7 @@ function resolveConfigPath(targetPath) { return path.resolve(normalised); } -const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp", "lmstudio", "bedrock", "zai", "vertex"]); +const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp", "lmstudio", "bedrock", "zai", "moonshot", "vertex"]); const rawModelProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase(); // Validate MODEL_PROVIDER early with a clear error message @@ -132,6 +132,11 @@ const zaiApiKey = process.env.ZAI_API_KEY?.trim() || null; const zaiEndpoint = process.env.ZAI_ENDPOINT?.trim() || "https://api.z.ai/api/anthropic/v1/messages"; const zaiModel = process.env.ZAI_MODEL?.trim() || "GLM-4.7"; +// Moonshot configuration - Anthropic-compatible API for Kimi models +const moonshotApiKey = process.env.MOONSHOT_API_KEY?.trim() || null; +const moonshotEndpoint = process.env.MOONSHOT_ENDPOINT?.trim() || "https://api.moonshot.ai/anthropic/v1/messages"; +const moonshotModel = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2.5"; + // Vertex AI (Google Gemini) configuration const vertexApiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null; const vertexModel = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash"; @@ -305,6 +310,12 @@ if (modelProvider === "bedrock" && !bedrockApiKey) { ); } +if (modelProvider === "moonshot" && !moonshotApiKey) { + throw new Error( + "Set MOONSHOT_API_KEY before starting the proxy.", + ); +} + // Validate hybrid routing configuration if (preferOllama) { if (!ollamaEndpoint) { @@ -319,7 +330,7 @@ if (preferOllama) { // Prevent local providers from being used as fallback (they can fail just like Ollama) const localProviders = ["ollama", "llamacpp", "lmstudio"]; if (fallbackEnabled && localProviders.includes(fallbackProvider)) { - throw new Error(`FALLBACK_PROVIDER cannot be '${fallbackProvider}' (local providers should not be fallbacks). Use cloud providers: databricks, azure-anthropic, azure-openai, openrouter, openai, bedrock`); + throw new Error(`FALLBACK_PROVIDER cannot be '${fallbackProvider}' (local providers should not be fallbacks). Use cloud providers: databricks, azure-anthropic, azure-openai, openrouter, openai, bedrock, zai, moonshot, vertex`); } // Ensure fallback provider is properly configured (only if fallback is enabled) @@ -336,6 +347,9 @@ if (preferOllama) { if (fallbackProvider === "bedrock" && !bedrockApiKey) { throw new Error("FALLBACK_PROVIDER is set to 'bedrock' but AWS_BEDROCK_API_KEY is not configured. Please set this environment variable or choose a different fallback provider."); } + if (fallbackProvider === "moonshot" && !moonshotApiKey) { + throw new Error("FALLBACK_PROVIDER is set to 'moonshot' but MOONSHOT_API_KEY is not configured. Please set this environment variable or choose a different fallback provider."); + } } } @@ -589,6 +603,11 @@ var config = { endpoint: zaiEndpoint, model: zaiModel, }, + moonshot: { + apiKey: moonshotApiKey, + endpoint: moonshotEndpoint, + model: moonshotModel, + }, vertex: { apiKey: vertexApiKey, model: vertexModel, @@ -878,7 +897,11 @@ function reloadConfig() { config.openai.apiKey = process.env.OPENAI_API_KEY?.trim() || null; config.bedrock.apiKey = process.env.AWS_BEDROCK_API_KEY?.trim() || null; config.zai.apiKey = process.env.ZAI_API_KEY?.trim() || null; + config.zai.endpoint = process.env.ZAI_ENDPOINT?.trim() || "https://api.z.ai/api/anthropic/v1/messages"; config.zai.model = process.env.ZAI_MODEL?.trim() || "GLM-4.7"; + config.moonshot.apiKey = process.env.MOONSHOT_API_KEY?.trim() || null; + config.moonshot.endpoint = process.env.MOONSHOT_ENDPOINT?.trim() || "https://api.moonshot.ai/anthropic/v1/messages"; + config.moonshot.model = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2.5"; config.vertex.apiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null; config.vertex.model = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash"; diff --git a/src/orchestrator/index.js b/src/orchestrator/index.js index 55a47a5..198a05a 100644 --- a/src/orchestrator/index.js +++ b/src/orchestrator/index.js @@ -47,6 +47,8 @@ function getDestinationUrl(providerType) { return config.bedrock?.endpoint ?? 'unknown'; case 'zai': return config.zai?.endpoint ?? 'unknown'; + case 'moonshot': + return config.moonshot?.endpoint ?? 'unknown'; case 'vertex': return config.vertex?.endpoint ?? 'unknown'; default: @@ -165,6 +167,23 @@ function flattenBlocks(blocks) { .join(""); } +function hasToolProtocolHistory(messages) { + if (!Array.isArray(messages) || messages.length === 0) return false; + return messages.some((message) => { + if (!message) return false; + if (message.role === "tool" || Array.isArray(message.tool_calls)) return true; + if (!Array.isArray(message.content)) return false; + return message.content.some((block) => { + if (!block || typeof block !== "object") return false; + return ( + block.type === "tool_use" || + block.type === "tool_result" || + block.type === "tool_reference" + ); + }); + }); +} + function normaliseMessages(payload, options = {}) { const flattenContent = options.flattenContent !== false; const normalised = []; @@ -207,6 +226,19 @@ function normaliseTools(tools) { })); } +function endpointUsesOpenAIChatCompletions(endpoint) { + return typeof endpoint === "string" && endpoint.includes("/chat/completions"); +} + +function providerUsesAnthropicToolProtocol(providerType) { + if (providerType === "azure-anthropic") return true; + if (providerType === "moonshot") return !endpointUsesOpenAIChatCompletions(config.moonshot?.endpoint); + if (providerType === "zai") return !endpointUsesOpenAIChatCompletions(config.zai?.endpoint); + if (providerType === "bedrock") return true; + if (providerType === "vertex") return true; + return false; +} + /** * Ensure tools are in Anthropic format for Databricks/Claude API * Databricks expects: {name, description, input_schema} @@ -527,7 +559,7 @@ function parseExecutionContent(content) { } function createFallbackAssistantMessage(providerType, { text, toolCall }) { - if (providerType === "azure-anthropic") { + if (providerUsesAnthropicToolProtocol(providerType)) { const blocks = []; if (typeof text === "string" && text.trim().length > 0) { blocks.push({ type: "text", text: text.trim() }); @@ -558,7 +590,7 @@ function createFallbackAssistantMessage(providerType, { text, toolCall }) { function createFallbackToolResultMessage(providerType, { toolCall, execution }) { const toolName = execution.name ?? toolCall.function?.name ?? "tool"; const toolId = execution.id ?? toolCall.id ?? `tool_${Date.now()}`; - if (providerType === "azure-anthropic") { + if (providerUsesAnthropicToolProtocol(providerType)) { const parsed = parseExecutionContent(execution.content); let contentBlocks; if (typeof parsed === "string" || parsed === null) { @@ -852,7 +884,7 @@ function sanitizePayload(payload) { "databricks-claude-sonnet-4-5"; clean.model = requestedModel; const providerType = config.modelProvider?.type ?? "databricks"; - const flattenContent = providerType !== "azure-anthropic"; + const flattenContent = !providerUsesAnthropicToolProtocol(providerType); clean.messages = normaliseMessages(clean, { flattenContent }).filter((msg) => { const hasToolCalls = Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0; @@ -870,7 +902,7 @@ function sanitizePayload(payload) { } return hasToolCalls; }); - if (providerType === "azure-anthropic") { + if (providerUsesAnthropicToolProtocol(providerType)) { const cleanedMessages = []; for (const message of clean.messages) { if (isPlaceholderToolResultMessage(message)) { @@ -1090,6 +1122,13 @@ function sanitizePayload(payload) { // Ensure tools are in Anthropic format clean.tools = ensureAnthropicToolFormat(clean.tools); } + } else if (providerType === "moonshot") { + // Moonshot uses Anthropic-compatible endpoint and tool format + if (!Array.isArray(clean.tools) || clean.tools.length === 0) { + delete clean.tools; + } else { + clean.tools = ensureAnthropicToolFormat(clean.tools); + } } else if (providerType === "vertex") { // Vertex AI supports tools - keep them in Anthropic format if (!Array.isArray(clean.tools) || clean.tools.length === 0) { @@ -1121,7 +1160,13 @@ function sanitizePayload(payload) { } // Smart tool selection (universal, applies to all providers) - if (config.smartToolSelection?.enabled && Array.isArray(clean.tools) && clean.tools.length > 0) { + const hasToolHistory = hasToolProtocolHistory(clean.messages); + if ( + config.smartToolSelection?.enabled && + Array.isArray(clean.tools) && + clean.tools.length > 0 && + !hasToolHistory + ) { const classification = classifyRequestType(clean); const selectedTools = selectToolsSmartly(clean.tools, classification, { provider: providerType, @@ -1348,6 +1393,7 @@ async function runAgentLoop({ let steps = 0; let toolCallsExecuted = 0; let fallbackPerformed = false; + const anthropicToolProtocol = providerUsesAnthropicToolProtocol(providerType); const toolCallNames = new Map(); const toolCallHistory = new Map(); // Track tool calls to detect loops: signature -> count let loopWarningInjected = false; // Track if we've already warned about loops @@ -1870,7 +1916,7 @@ IMPORTANT TOOL USAGE RULES: // Detect Anthropic format: has 'content' array and 'stop_reason' at top level (no 'choices') // This handles azure-anthropic provider AND azure-openai Responses API (which we convert to Anthropic format) - const isAnthropicFormat = providerType === "azure-anthropic" || + const isAnthropicFormat = anthropicToolProtocol || (Array.isArray(databricksResponse.json?.content) && databricksResponse.json?.stop_reason !== undefined && !databricksResponse.json?.choices); if (isAnthropicFormat) { @@ -1929,7 +1975,7 @@ IMPORTANT TOOL USAGE RULES: if (toolCalls.length > 0) { // Convert OpenAI/OpenRouter format to Anthropic format for session storage let sessionContent; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { // Azure Anthropic already returns content in Anthropic sessionContent = databricksResponse.json?.content ?? []; } else { @@ -1990,7 +2036,7 @@ IMPORTANT TOOL USAGE RULES: }); let assistantToolMessage; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { // For Azure Anthropic, use the content array directly from the response // It already contains both text and tool_use blocks in the correct format assistantToolMessage = { @@ -2007,7 +2053,7 @@ IMPORTANT TOOL USAGE RULES: // Only add fallback content for Databricks format (Azure already has content) if ( - providerType !== "azure-anthropic" && + !anthropicToolProtocol && (!assistantToolMessage.content || (typeof assistantToolMessage.content === "string" && assistantToolMessage.content.trim().length === 0)) && @@ -2188,7 +2234,7 @@ IMPORTANT TOOL USAGE RULES: toolCallsExecuted += 1; let toolMessage; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { const parsedContent = parseExecutionContent(execution.content); const serialisedContent = typeof parsedContent === "string" || parsedContent === null @@ -2227,7 +2273,7 @@ IMPORTANT TOOL USAGE RULES: // Convert to Anthropic format for session storage let sessionToolResultContent; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { sessionToolResultContent = toolMessage.content; } else { sessionToolResultContent = [ @@ -2321,7 +2367,7 @@ IMPORTANT TOOL USAGE RULES: ); let toolResultMessage; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { // Anthropic format: tool_result in user message content array toolResultMessage = { role: "user", @@ -2348,7 +2394,7 @@ IMPORTANT TOOL USAGE RULES: // Convert to Anthropic format for session storage let sessionToolResult; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { sessionToolResult = toolResultMessage.content; } else { // Convert OpenRouter tool message to Anthropic format @@ -2415,7 +2461,7 @@ IMPORTANT TOOL USAGE RULES: }); let toolMessage; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { const parsedContent = parseExecutionContent(execution.content); const serialisedContent = typeof parsedContent === "string" || parsedContent === null @@ -2475,7 +2521,7 @@ IMPORTANT TOOL USAGE RULES: // Convert to Anthropic format for session storage let sessionToolResultContent; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { // Azure Anthropic already has content in correct format sessionToolResultContent = toolMessage.content; } else { @@ -2849,12 +2895,12 @@ IMPORTANT TOOL USAGE RULES: }, "=== CONVERTED ANTHROPIC RESPONSE (llama.cpp) ==="); anthropicPayload.content = policy.sanitiseContent(anthropicPayload.content); - } else if (actualProvider === "zai") { - // Z.AI responses are already converted to Anthropic format in invokeZai + } else if (actualProvider === "zai" || actualProvider === "moonshot") { + // Z.AI/Moonshot responses are already converted to Anthropic format logger.info({ hasJson: !!databricksResponse.json, jsonContent: JSON.stringify(databricksResponse.json?.content)?.substring(0, 200), - }, "=== ZAI ORCHESTRATOR DEBUG ==="); + }, "=== ANTHROPIC-COMPAT ORCHESTRATOR DEBUG ==="); anthropicPayload = databricksResponse.json; if (Array.isArray(anthropicPayload?.content)) { anthropicPayload.content = policy.sanitiseContent(anthropicPayload.content); @@ -3002,7 +3048,7 @@ IMPORTANT TOOL USAGE RULES: // Convert to Anthropic format for session storage let sessionFallbackContent; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { // Already in Anthropic format sessionFallbackContent = assistantToolMessage.content; } else { @@ -3070,7 +3116,7 @@ IMPORTANT TOOL USAGE RULES: // Convert to Anthropic format for session storage let sessionFallbackToolResult; - if (providerType === "azure-anthropic") { + if (anthropicToolProtocol) { // Already in Anthropic format sessionFallbackToolResult = toolResultMessage.content; } else { @@ -3205,6 +3251,10 @@ IMPORTANT TOOL USAGE RULES: response: { status: 200, body: anthropicPayload, + headers: { + "X-Lynkr-Provider": databricksResponse.actualProvider || providerType, + "X-Lynkr-Routing-Method": databricksResponse.routingDecision?.method || "static", + }, terminationReason: "completion", }, steps,