Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions apps/gateway/src/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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" ||
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/extract-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("") || "";
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/extract-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/extract-reasoning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("") || "";
Expand Down
1 change: 1 addition & 0 deletions apps/gateway/src/chat/tools/extract-token-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/extract-tool-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/parse-provider-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/transform-response-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions apps/gateway/src/lib/costs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion apps/gateway/src/lib/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
40 changes: 40 additions & 0 deletions packages/actions/src/get-provider-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions packages/actions/src/get-provider-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function getProviderHeaders(
}
case "google-ai-studio":
case "google-vertex":
case "avalanche":
return {};
case "obsidian":
return {
Expand Down
3 changes: 2 additions & 1 deletion packages/actions/src/prepare-request-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading