diff --git a/apps/gateway/src/chat/chat.ts b/apps/gateway/src/chat/chat.ts index d855c5d07c..516556e7a2 100644 --- a/apps/gateway/src/chat/chat.ts +++ b/apps/gateway/src/chat/chat.ts @@ -1503,7 +1503,9 @@ chat.openapi(completions, async (c) => { usedProvider, providerKey?.baseUrl || undefined, usedModel, - usedProvider === "google-ai-studio" || usedProvider === "google-vertex" + usedProvider === "google-ai-studio" || + usedProvider === "google-vertex" || + usedProvider === "avalanche" ? usedToken : undefined, stream, @@ -2009,7 +2011,8 @@ chat.openapi(completions, async (c) => { if ( usedProvider === "google-ai-studio" || usedProvider === "google-vertex" || - usedProvider === "obsidian" + usedProvider === "obsidian" || + usedProvider === "avalanche" ) { const { redisClient } = await import("@llmgateway/cache"); for (const message of messages) { @@ -3851,7 +3854,8 @@ chat.openapi(completions, async (c) => { if ( usedProvider === "google-ai-studio" || usedProvider === "google-vertex" || - usedProvider === "obsidian" + usedProvider === "obsidian" || + usedProvider === "avalanche" ) { const parts = data.candidates?.[0]?.content?.parts || []; for (const part of parts) { @@ -3878,7 +3882,8 @@ chat.openapi(completions, async (c) => { } else if ( usedProvider === "google-ai-studio" || usedProvider === "google-vertex" || - usedProvider === "obsidian" + usedProvider === "obsidian" || + usedProvider === "avalanche" ) { // For Google, count when grounding metadata is present if (data.candidates?.[0]?.groundingMetadata) { @@ -3974,6 +3979,7 @@ chat.openapi(completions, async (c) => { case "google-ai-studio": case "google-vertex": case "obsidian": + case "avalanche": // Preserve original Google finish reason for logging if (data.promptFeedback?.blockReason) { finishReason = data.promptFeedback.blockReason; @@ -4372,7 +4378,8 @@ chat.openapi(completions, async (c) => { const providerExcludesImageInput = usedProvider === "google-ai-studio" || usedProvider === "google-vertex" || - usedProvider === "obsidian"; + usedProvider === "obsidian" || + usedProvider === "avalanche"; const imageInputAdj = providerExcludesImageInput ? inputImageCount * 560 : 0; @@ -5806,7 +5813,8 @@ chat.openapi(completions, async (c) => { if ( usedProvider === "google-ai-studio" || usedProvider === "google-vertex" || - usedProvider === "obsidian" + usedProvider === "obsidian" || + usedProvider === "avalanche" ) { logger.debug("Google model response parsed", { usedProvider, @@ -5984,7 +5992,8 @@ chat.openapi(completions, async (c) => { const isGoogleContentFilter = (usedProvider === "google-ai-studio" || usedProvider === "google-vertex" || - usedProvider === "obsidian") && + usedProvider === "obsidian" || + usedProvider === "avalanche") && (finishReason === "SAFETY" || finishReason === "PROHIBITED_CONTENT" || finishReason === "RECITATION" || diff --git a/apps/gateway/src/chat/tools/extract-content.ts b/apps/gateway/src/chat/tools/extract-content.ts index d85cf6a75d..459af695be 100644 --- a/apps/gateway/src/chat/tools/extract-content.ts +++ b/apps/gateway/src/chat/tools/extract-content.ts @@ -7,7 +7,8 @@ export function extractContent(data: any, provider: Provider): string { switch (provider) { case "google-ai-studio": case "google-vertex": - case "obsidian": { + case "obsidian": + case "avalanche": { const parts = data.candidates?.[0]?.content?.parts || []; const contentParts = parts.filter((part: any) => !part.thought); return contentParts.map((part: any) => part.text).join("") || ""; diff --git a/apps/gateway/src/chat/tools/extract-images.ts b/apps/gateway/src/chat/tools/extract-images.ts index 1b15f6d071..7908d250cd 100644 --- a/apps/gateway/src/chat/tools/extract-images.ts +++ b/apps/gateway/src/chat/tools/extract-images.ts @@ -12,7 +12,8 @@ export function extractImages(data: any, provider: Provider): ImageObject[] { switch (provider) { case "google-ai-studio": case "google-vertex": - case "obsidian": { + case "obsidian": + case "avalanche": { const parts = data.candidates?.[0]?.content?.parts || []; const imageParts = parts.filter((part: any) => part.inlineData); return imageParts.map( diff --git a/apps/gateway/src/chat/tools/extract-reasoning.ts b/apps/gateway/src/chat/tools/extract-reasoning.ts index a74cc17b88..8d833dc3cc 100644 --- a/apps/gateway/src/chat/tools/extract-reasoning.ts +++ b/apps/gateway/src/chat/tools/extract-reasoning.ts @@ -19,7 +19,8 @@ export function extractReasoning(data: any, provider: Provider): string { } case "google-ai-studio": case "google-vertex": - case "obsidian": { + case "obsidian": + case "avalanche": { const parts = data.candidates?.[0]?.content?.parts || []; const reasoningParts = parts.filter((part: any) => part.thought); return reasoningParts.map((part: any) => part.text).join("") || ""; diff --git a/apps/gateway/src/chat/tools/extract-token-usage.ts b/apps/gateway/src/chat/tools/extract-token-usage.ts index 911b09cc00..4fa5368836 100644 --- a/apps/gateway/src/chat/tools/extract-token-usage.ts +++ b/apps/gateway/src/chat/tools/extract-token-usage.ts @@ -50,6 +50,7 @@ export function extractTokenUsage( case "google-ai-studio": case "google-vertex": case "obsidian": + case "avalanche": if (data.usageMetadata) { promptTokens = data.usageMetadata.promptTokenCount ?? null; let rawCandidates = data.usageMetadata.candidatesTokenCount ?? null; diff --git a/apps/gateway/src/chat/tools/extract-tool-calls.ts b/apps/gateway/src/chat/tools/extract-tool-calls.ts index e1ad2f3763..ff4ea29c3b 100644 --- a/apps/gateway/src/chat/tools/extract-tool-calls.ts +++ b/apps/gateway/src/chat/tools/extract-tool-calls.ts @@ -40,7 +40,8 @@ export function extractToolCalls(data: any, provider: Provider): any[] | null { return null; case "google-ai-studio": case "google-vertex": - case "obsidian": { + case "obsidian": + case "avalanche": { // Google AI Studio tool calls in streaming // Include thoughtSignature if present (required for Gemini 3 multi-turn conversations) // Note: Redis caching of thought_signature happens in transform-streaming-to-openai.ts diff --git a/apps/gateway/src/chat/tools/parse-provider-response.ts b/apps/gateway/src/chat/tools/parse-provider-response.ts index a3cea77d3b..51d0ca9331 100644 --- a/apps/gateway/src/chat/tools/parse-provider-response.ts +++ b/apps/gateway/src/chat/tools/parse-provider-response.ts @@ -181,7 +181,8 @@ export function parseProviderResponse( } case "google-ai-studio": case "google-vertex": - case "obsidian": { + case "obsidian": + case "avalanche": { // Check if response is missing candidates - treat as content filter if (!json.candidates || json.candidates.length === 0) { // Only log warning if there's no blockReason explaining why diff --git a/apps/gateway/src/chat/tools/transform-response-to-openai.ts b/apps/gateway/src/chat/tools/transform-response-to-openai.ts index 6da614d7ae..8078eb2782 100644 --- a/apps/gateway/src/chat/tools/transform-response-to-openai.ts +++ b/apps/gateway/src/chat/tools/transform-response-to-openai.ts @@ -87,7 +87,8 @@ export function transformResponseToOpenai( switch (usedProvider) { case "google-ai-studio": case "google-vertex": - case "obsidian": { + case "obsidian": + case "avalanche": { transformedResponse = { id: `chatcmpl-${Date.now()}`, object: "chat.completion", diff --git a/apps/gateway/src/chat/tools/transform-streaming-to-openai.ts b/apps/gateway/src/chat/tools/transform-streaming-to-openai.ts index 6df339f12c..ac1b302959 100644 --- a/apps/gateway/src/chat/tools/transform-streaming-to-openai.ts +++ b/apps/gateway/src/chat/tools/transform-streaming-to-openai.ts @@ -251,7 +251,8 @@ export function transformStreamingToOpenai( case "google-ai-studio": case "google-vertex": - case "obsidian": { + case "obsidian": + case "avalanche": { const mapFinishReason = ( finishReason?: string, hasFunctionCalls?: boolean, diff --git a/apps/gateway/src/lib/costs.ts b/apps/gateway/src/lib/costs.ts index 328bb0d77a..5e26fb1163 100644 --- a/apps/gateway/src/lib/costs.ts +++ b/apps/gateway/src/lib/costs.ts @@ -312,7 +312,8 @@ export async function calculateCosts( const isGoogleProvider = provider === "google-ai-studio" || provider === "google-vertex" || - provider === "obsidian"; + provider === "obsidian" || + provider === "avalanche"; const totalOutputTokens = isGoogleProvider ? calculatedCompletionTokens : calculatedCompletionTokens + (reasoningTokens || 0); @@ -384,7 +385,8 @@ export async function calculateCosts( imageInputTokens && (provider === "google-ai-studio" || provider === "google-vertex" || - provider === "obsidian") + provider === "obsidian" || + provider === "avalanche") ? (calculatedPromptTokens || 0) + imageInputTokens : calculatedPromptTokens, completionTokens: calculatedCompletionTokens, diff --git a/apps/gateway/src/lib/logs.ts b/apps/gateway/src/lib/logs.ts index 827426d5e2..37a5ed1800 100644 --- a/apps/gateway/src/lib/logs.ts +++ b/apps/gateway/src/lib/logs.ts @@ -21,7 +21,8 @@ export function isExpectedUnknownFinishReason( if ( (provider === "google-ai-studio" || provider === "google-vertex" || - provider === "obsidian") && + provider === "obsidian" || + provider === "avalanche") && finishReason === "OTHER" ) { return true; @@ -74,6 +75,7 @@ export function getUnifiedFinishReason( case "google-ai-studio": case "google-vertex": case "obsidian": + case "avalanche": // Google finish reasons (original format, not mapped to OpenAI) if (finishReason === "STOP") { return UnifiedFinishReason.COMPLETED; diff --git a/packages/actions/src/get-provider-endpoint.ts b/packages/actions/src/get-provider-endpoint.ts index a84e745010..5b0236bcbc 100644 --- a/packages/actions/src/get-provider-endpoint.ts +++ b/packages/actions/src/get-provider-endpoint.ts @@ -69,6 +69,14 @@ export function getProviderEndpoint( ); } break; + case "avalanche": + url = getProviderEnvValue("avalanche", "baseUrl", configIndex); + if (!url) { + throw new Error( + "Avalanche provider requires LLM_AVALANCHE_BASE_URL environment variable", + ); + } + break; case "inference.net": url = "https://api.inference.net"; break; @@ -198,6 +206,38 @@ export function getProviderEndpoint( ? `${baseEndpoint}?${queryParams.join("&")}` : baseEndpoint; } + case "avalanche": { + const endpoint = stream ? "streamGenerateContent" : "generateContent"; + const model = modelName || "gemini-3-pro-image-preview"; + + const projectId = getProviderEnvValue( + "avalanche", + "project", + configIndex, + ); + + const region = + getProviderEnvValue("avalanche", "region", configIndex, "global") || + "global"; + + if (!projectId) { + throw new Error( + "Avalanche provider requires LLM_AVALANCHE_PROJECT environment variable", + ); + } + + const baseEndpoint = `${url}/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${endpoint}`; + const queryParams = []; + if (token) { + queryParams.push(`key=${token}`); + } + if (stream) { + queryParams.push("alt=sse"); + } + return queryParams.length > 0 + ? `${baseEndpoint}?${queryParams.join("&")}` + : baseEndpoint; + } case "google-vertex": { const endpoint = stream ? "streamGenerateContent" : "generateContent"; const model = modelName || "gemini-2.5-flash-lite"; diff --git a/packages/actions/src/get-provider-headers.ts b/packages/actions/src/get-provider-headers.ts index 3db1acb1d0..757d00632e 100644 --- a/packages/actions/src/get-provider-headers.ts +++ b/packages/actions/src/get-provider-headers.ts @@ -29,6 +29,7 @@ export function getProviderHeaders( } case "google-ai-studio": case "google-vertex": + case "avalanche": return {}; case "obsidian": return { diff --git a/packages/actions/src/prepare-request-body.ts b/packages/actions/src/prepare-request-body.ts index 264b45e192..8d5fa52673 100644 --- a/packages/actions/src/prepare-request-body.ts +++ b/packages/actions/src/prepare-request-body.ts @@ -1340,7 +1340,8 @@ export async function prepareRequestBody( } case "google-ai-studio": case "google-vertex": - case "obsidian": { + case "obsidian": + case "avalanche": { delete requestBody.model; // Not used in body delete requestBody.stream; // Stream is handled via URL parameter delete requestBody.messages; // Not used in body for Google providers diff --git a/packages/models/src/models/google.ts b/packages/models/src/models/google.ts index 7b62b43c2b..4df8d24faf 100644 --- a/packages/models/src/models/google.ts +++ b/packages/models/src/models/google.ts @@ -562,6 +562,43 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "avalanche", + modelName: "gemini-3-pro-preview", + inputPrice: 2 / 1e6, + outputPrice: 12 / 1e6, + cachedInputPrice: 0.2 / 1e6, + pricingTiers: [ + { + name: "Up to 200K", + upToTokens: 200000, + inputPrice: 2 / 1e6, + outputPrice: 12 / 1e6, + cachedInputPrice: 0.2 / 1e6, + }, + { + name: "Over 200K", + upToTokens: Infinity, + inputPrice: 4 / 1e6, + outputPrice: 18 / 1e6, + cachedInputPrice: 0.4 / 1e6, + }, + ], + requestPrice: 0, + discount: 0.2, + contextSize: 1048576, + maxOutput: 65536, + reasoning: true, + reasoningMaxTokens: true, + streaming: true, + vision: true, + tools: true, + webSearch: true, + webSearchPrice: 0.014, + jsonOutput: true, + jsonOutputSchema: true, + }, ], }, { @@ -642,6 +679,43 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "avalanche", + modelName: "gemini-3.1-pro-preview", + inputPrice: 2 / 1e6, + outputPrice: 12 / 1e6, + cachedInputPrice: 0.2 / 1e6, + pricingTiers: [ + { + name: "Up to 200K", + upToTokens: 200000, + inputPrice: 2 / 1e6, + outputPrice: 12 / 1e6, + cachedInputPrice: 0.2 / 1e6, + }, + { + name: "Over 200K", + upToTokens: Infinity, + inputPrice: 4 / 1e6, + outputPrice: 18 / 1e6, + cachedInputPrice: 0.4 / 1e6, + }, + ], + requestPrice: 0, + discount: 0.2, + contextSize: 1048576, + maxOutput: 65536, + reasoning: true, + reasoningMaxTokens: true, + streaming: true, + vision: true, + tools: true, + webSearch: true, + webSearchPrice: 0.014, + jsonOutput: true, + jsonOutputSchema: true, + }, ], }, { @@ -690,6 +764,27 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "avalanche", + modelName: "gemini-3-flash-preview", + inputPrice: 0.5 / 1e6, + outputPrice: 3 / 1e6, + cachedInputPrice: 0.05 / 1e6, + requestPrice: 0, + discount: 0.2, + contextSize: 1048576, + maxOutput: 65535, + reasoning: true, + reasoningMaxTokens: true, + streaming: true, + vision: true, + tools: true, + webSearch: true, + webSearchPrice: 0.014, + jsonOutput: true, + jsonOutputSchema: true, + }, ], }, { @@ -877,6 +972,25 @@ export const googleModels = [ jsonOutput: true, jsonOutputSchema: true, }, + { + test: "skip", + providerId: "avalanche", + modelName: "gemini-3-pro-image-preview", + inputPrice: 2 / 1e6, + outputPrice: 12 / 1e6, + cachedInputPrice: 0.2 / 1e6, + imageInputPrice: 2 / 1e6, + imageOutputPrice: 120 / 1e6, + requestPrice: 0, + discount: 0.2, + contextSize: 65536, + maxOutput: 32768, + streaming: true, + vision: true, + tools: false, + jsonOutput: true, + jsonOutputSchema: true, + }, ], }, { diff --git a/packages/models/src/providers.ts b/packages/models/src/providers.ts index 9c04407b9b..84544edb59 100644 --- a/packages/models/src/providers.ts +++ b/packages/models/src/providers.ts @@ -132,6 +132,28 @@ export const providers = [ color: "#1a1a1a", website: null, announcement: null, + priority: 1.5, + }, + { + id: "avalanche", + name: "Avalanche", + description: "Avalanche - Google Vertex-compatible LLM provider.", + env: { + required: { + apiKey: "LLM_AVALANCHE_API_KEY", + baseUrl: "LLM_AVALANCHE_BASE_URL", + project: "LLM_AVALANCHE_PROJECT", + }, + optional: { + region: "LLM_AVALANCHE_REGION", + }, + }, + streaming: true, + cancellation: true, + color: "#1a1a1a", + website: null, + announcement: null, + priority: 1.3, }, { id: "groq",