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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ LLM_GONKA_24_API_KEY=your_gonka24_key_here
LLM_FIREWORKS_API_KEY=your_fireworks_key_here
LLM_FIREWORKS_BASE_URL=https://api.fireworks.ai/inference

# RanoAI
LLM_RANOAI_API_KEY=your_ranoai_key_here

# Google Vertex AI (Gemini models)
LLM_GOOGLE_VERTEX_API_KEY=your_google_vertex_api_key_here
LLM_GOOGLE_CLOUD_PROJECT=your_google_cloud_project_id
Expand Down
3 changes: 3 additions & 0 deletions .env.unified.example
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ LLM_TOGETHER_AI_API_KEY=your_together_ai_key_here
# SCX.ai
LLM_SCX_AI_API_KEY=your_scx_ai_key_here

# RanoAI
LLM_RANOAI_API_KEY=your_ranoai_key_here

# Mistral
LLM_MISTRAL_API_KEY=your_mistral_key_here

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ jobs:
LLM_META_API_KEY: ${{ secrets.LLM_META_API_KEY }}
LLM_GONKA_24_API_KEY: ${{ secrets.LLM_GONKA_24_API_KEY }}
LLM_FIREWORKS_API_KEY: ${{ secrets.LLM_FIREWORKS_API_KEY }}
LLM_RANOAI_API_KEY: ${{ secrets.LLM_RANOAI_API_KEY }}

- name: Upload shard results
if: always()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,7 @@ export function transformStreamingToOpenai(
case "embercloud":
case "runware":
case "gonka24":
case "ranoai":
case "granite":
case "tundra":
case "xiaomi":
Expand Down
41 changes: 41 additions & 0 deletions apps/gateway/src/lib/costs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,47 @@ describe("calculateCosts", () => {
expect(result.estimatedCost).toBe(false);
});

it("should bill RanoAI cached tokens at the cache-read rate", async () => {
// RanoAI does automatic prefix caching and charges input_cache_read
// (0.05/M), half the input rate. Without cachedInputPrice the engine
// falls back to inputPrice and would bill cached tokens at 2x.
const result = await calculateCosts(
"gemma-4-31b-it",
"ranoai",
null,
2521,
10,
2496, // cachedTokens
);

// 25 uncached * 0.1e-6 + 2496 cached * 0.05e-6
expect(result.inputCost).toBeCloseTo(0.0000025, 10);
expect(result.cachedInputCost).toBeCloseTo(0.0001248, 10);
});

it("should not double-bill RanoAI reasoning tokens", async () => {
// RanoAI reports reasoning in completion_tokens_details (hoisted to a
// top-level reasoning_tokens by the streaming transform) while already
// counting it inside completion_tokens, so it must not be added again.
const result = await calculateCosts(
"gemma-4-31b-it",
"ranoai",
null,
1000,
330, // completionTokens already includes the 267 reasoning tokens
null,
undefined,
267,
);

// inputPrice 0.1e-6, outputPrice 0.3e-6. Precision matters here: billing
// 330 vs 597 output tokens differs by only 8e-5, which the default
// two-decimal toBeCloseTo would not catch.
expect(result.inputCost).toBeCloseTo(0.0001, 10);
expect(result.outputCost).toBeCloseTo(0.000099, 10); // 330 * 0.3e-6, not 597
expect(result.completionTokens).toBe(330);
});

it("should handle null reasoning tokens gracefully", async () => {
const result = await calculateCosts(
"gemini-2.5-pro",
Expand Down
9 changes: 7 additions & 2 deletions apps/gateway/src/lib/costs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,8 +643,12 @@ export async function calculateCosts(
// For Google models, completionTokens already includes reasoning tokens
// (merged during extraction). The same holds for OpenAI-style Responses API
// providers (OpenAI, Azure, Sakana, Meta), whose `output_tokens` counts
// reasoning — their `reasoning_tokens` detail is informational only. For
// remaining providers, add reasoning separately.
// reasoning — their `reasoning_tokens` detail is informational only. RanoAI
// reports reasoning in `completion_tokens_details` (which the streaming
// transform hoists to a top-level `reasoning_tokens`) while already counting
// it inside `completion_tokens`, so adding it again would roughly double the
// billed output on reasoning requests. For remaining providers, add
// reasoning separately.
const completionIncludesReasoning =
provider === "google-ai-studio" ||
provider === "glacier" ||
Expand All @@ -655,6 +659,7 @@ export async function calculateCosts(
provider === "azure" ||
provider === "sakana" ||
provider === "meta" ||
provider === "ranoai" ||
provider === "aws-mantle";
const totalOutputTokens = completionIncludesReasoning
? calculatedCompletionTokens
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/components/provider-keys/provider-logo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const providerLogoUrls: Partial<
gonka24: ProviderIcons.gonka24,
fireworks: ProviderIcons.fireworks,
runware: ProviderIcons.runware,
ranoai: ProviderIcons.ranoai,
};

export const getProviderLogoDarkModeClasses = () => {
Expand Down
1 change: 1 addition & 0 deletions infra/helm/llmgateway/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ llmProviders:
# LLM_PERPLEXITY_API_KEY: ""
# LLM_TOGETHER_AI_API_KEY: ""
# LLM_SCX_AI_API_KEY: ""
# LLM_RANOAI_API_KEY: ""
# LLM_ALIBABA_API_KEY: ""
# LLM_NEBIUS_API_KEY: ""
# LLM_CEREBRAS_API_KEY: ""
Expand Down
2 changes: 2 additions & 0 deletions packages/actions/src/get-provider-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ const PROVIDER_DEFAULT_BASE_URLS: Partial<Record<ProviderId, string>> = {
deepinfra: "https://api.deepinfra.com/v1/openai",
gonka24: "https://api.gonka24.com",
fireworks: "https://api.fireworks.ai/inference",
ranoai: "https://api.ranoai.com",
};

export function getProviderDefaultBaseUrl(
Expand Down Expand Up @@ -865,6 +866,7 @@ export function getProviderEndpoint(
case "tundra":
case "scx-ai":
case "scx-ai-gp":
case "ranoai":
case "custom":
default:
return `${url}/v1/chat/completions`;
Expand Down
39 changes: 39 additions & 0 deletions packages/models/src/models/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2450,6 +2450,45 @@ export const googleModels = [
tools: true,
jsonOutput: true,
},
{
// RanoAI serves Gemma 4 on Furiosa RNGD NPUs. The NPU deployment
// enforces a 20480-token window (prompt + max_tokens). Reasoning
// arrives as `reasoning_content` (streamed as deltas) only when
// `reasoning_effort` is passed explicitly — a request without it
// returns no reasoning at all, and "none" suppresses it.
// `reasoning_tokens` is reported inside completion_tokens, so
// costs.ts lists this provider in completionIncludesReasoning.
//
// All four tool_choice modes are honoured, so none are declared
// here. Prompt caching is automatic prefix caching — the first
// request misses and later ones report `cached_tokens` — priced
// at the `input_cache_read` rate /v1/models advertises, half the
// input rate (verified 2026-08-05).
providerId: "ranoai",
externalId: "gemma-4-31b",
inputPrice: "0.1e-6",
outputPrice: "0.3e-6",
cachedInputPrice: "0.05e-6",
requestPrice: "0",
contextSize: 20480,
maxOutput: 20480,
streaming: true,
reasoning: true,
reasoningEfforts: [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
],
vision: true,
tools: true,
jsonOutput: true,
jsonOutputSchema: true,
supportsN: true,
},
],
},
{
Expand Down
26 changes: 26 additions & 0 deletions packages/models/src/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,32 @@ export const providers: ProviderDefinition[] = [
gdpr: true,
},
},
{
id: "ranoai",
name: "RanoAI",
description:
"RanoAI serves open-weight large language models on Furiosa RNGD NPU hardware via an OpenAI-compatible inference API.",
env: {
required: {
apiKey: "LLM_RANOAI_API_KEY",
},
},
streaming: true,
cancellation: true,
color: "#000000",
website: "https://ranoai.com",
statusPageUrl: null,
announcement: null,
termsUrl: "https://ranoai.com/terms",
privacyPolicyUrl: "https://ranoai.com/privacy",
headquarters: "US",
dataPolicy: {
apiTraining: false,
consumerTraining: false,
promptLogging: false,
retentionPeriod: "0 days",
},
},
] as const satisfies ProviderDefinition[];

export type ProviderId = (typeof providers)[number]["id"];
Expand Down
17 changes: 17 additions & 0 deletions packages/shared/src/components/provider-icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,21 @@ export const FireworksIconStatic: React.FC<React.SVGProps<SVGSVGElement>> = (
</svg>
);

export const RanoAIIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" {...props}>
<g
transform="translate(0,512) scale(0.1,-0.1)"
fill="currentColor"
stroke="none"
>
<path d="M0 2560 l0 -2560 2560 0 2560 0 0 2560 0 2560 -2560 0 -2560 0 0 -2560z m1392 1482 c66 -29 143 -104 175 -171 23 -47 28 -69 28 -141 0 -60 -6 -99 -19 -135 -27 -69 -102 -146 -179 -181 -54 -25 -74 -29 -152 -29 -71 0 -100 5 -137 22 -66 30 -143 105 -175 172 -24 48 -28 68 -28 146 0 73 5 100 23 140 41 88 121 157 227 192 57 19 179 11 237 -15z m1300 0 c66 -29 143 -104 175 -171 23 -47 28 -69 28 -141 0 -60 -6 -99 -19 -135 -27 -69 -102 -146 -179 -181 -54 -25 -74 -29 -152 -29 -71 0 -100 5 -137 22 -66 30 -143 105 -175 172 -24 48 -28 68 -28 146 0 73 5 100 23 140 41 88 121 157 227 192 57 19 179 11 237 -15z m1250 0 c66 -29 143 -104 175 -171 23 -47 28 -69 28 -141 0 -60 -6 -99 -19 -135 -27 -69 -102 -146 -179 -181 -54 -25 -74 -29 -152 -29 -71 0 -100 5 -137 22 -66 30 -143 105 -175 172 -24 48 -28 68 -28 146 0 73 5 100 23 140 41 88 121 157 227 192 57 19 179 11 237 -15z m-2550 -1280 c66 -29 143 -104 175 -171 23 -47 28 -69 28 -141 0 -60 -6 -99 -19 -135 -27 -69 -102 -146 -179 -181 -54 -25 -74 -29 -152 -29 -71 0 -100 5 -137 22 -66 30 -143 105 -175 172 -24 48 -28 68 -28 146 0 73 5 100 23 140 41 88 121 157 227 192 57 19 179 11 237 -15z m1310 0 c66 -29 143 -104 175 -171 23 -47 28 -69 28 -141 0 -60 -6 -99 -19 -135 -27 -69 -102 -146 -179 -181 -54 -25 -74 -29 -152 -29 -71 0 -100 5 -137 22 -66 30 -143 105 -175 172 -24 48 -28 68 -28 146 0 73 5 100 23 140 41 88 121 157 227 192 57 19 179 11 237 -15z m1240 0 c66 -29 143 -104 175 -171 23 -47 28 -69 28 -141 0 -60 -6 -99 -19 -135 -27 -69 -102 -146 -179 -181 -54 -25 -74 -29 -152 -29 -71 0 -100 5 -137 22 -66 30 -143 105 -175 172 -24 48 -28 68 -28 146 0 73 5 100 23 140 41 88 121 157 227 192 57 19 179 11 237 -15z m83 -1277 l0 -60 -50 0 -50 0 -3 49 c-5 71 0 78 55 74 l48 -3 0 -60z m-2636 -161 l40 -16 -19 -46 c-11 -28 -24 -46 -32 -44 -52 14 -80 13 -104 -3 -48 -31 -57 -73 -62 -277 l-4 -188 -49 0 -49 0 0 290 0 290 45 0 c45 0 45 0 45 -35 l0 -36 38 41 c44 46 82 52 151 24z m478 -14 c70 -39 76 -63 83 -305 4 -141 10 -217 18 -232 13 -23 12 -23 -47 -23 -57 0 -60 1 -66 29 l-6 29 -65 -34 c-58 -30 -74 -34 -142 -34 -61 0 -85 5 -117 23 -54 30 -88 96 -81 155 12 92 72 143 189 162 194 30 202 33 205 62 8 71 -54 114 -154 106 -60 -5 -89 -25 -110 -75 -13 -29 -16 -31 -56 -26 -24 3 -47 9 -51 13 -14 12 32 105 62 128 77 57 256 68 338 22z m588 11 c93 -43 104 -79 105 -353 l0 -218 -54 0 -55 0 -3 221 -3 221 -28 23 c-40 35 -86 41 -140 17 -75 -33 -81 -54 -85 -284 l-4 -198 -49 0 -49 0 0 290 0 290 45 0 45 0 0 -37 0 -37 28 27 c60 57 169 74 247 38z m578 3 c64 -22 131 -89 152 -155 20 -59 23 -184 6 -247 -17 -61 -68 -122 -131 -155 -51 -27 -61 -29 -140 -25 -94 5 -126 17 -180 72 -52 52 -75 122 -75 232 1 80 4 97 30 149 48 96 126 144 236 145 30 0 77 -7 102 -16z m661 -9 c77 -38 80 -49 87 -288 3 -126 10 -223 17 -240 7 -16 12 -30 12 -33 0 -2 -25 -4 -55 -4 -54 0 -56 1 -61 30 -4 17 -8 30 -10 30 -1 0 -26 -15 -55 -32 -50 -31 -57 -33 -150 -33 -109 0 -132 10 -177 76 -20 28 -23 44 -20 91 8 106 65 151 227 175 140 20 155 26 159 52 10 69 -38 111 -128 111 -68 0 -113 -25 -131 -73 -12 -33 -13 -34 -58 -29 -25 2 -49 7 -53 11 -10 9 35 102 61 125 63 57 249 73 335 31z m334 -277 l2 -288 -55 0 -55 0 0 291 0 290 53 -3 52 -3 3 -287z" />
<path d="M1720 1005 c-58 -12 -113 -26 -123 -30 -44 -18 -50 -89 -11 -126 53 -50 183 -23 229 47 18 28 33 135 19 133 -5 -1 -56 -12 -114 -24z" />
<path d="M2859 1227 c-73 -49 -104 -146 -81 -255 20 -96 72 -142 158 -142 99 0 157 75 158 204 0 85 -15 134 -52 169 -54 52 -128 61 -183 24z" />
<path d="M3648 1025 c-3 -3 -52 -14 -109 -25 -87 -16 -108 -24 -127 -46 -18 -21 -22 -34 -18 -58 17 -88 159 -100 237 -20 28 29 33 43 37 94 4 53 -3 72 -20 55z" />
</g>
</svg>
);

export const DeepInfraIcon: React.FC<React.SVGProps<SVGSVGElement>> = (
props,
) => (
Expand Down Expand Up @@ -1586,6 +1601,7 @@ export const ProviderIcons = {
gonka24: Gonka24Icon,
fireworks: FireworksIcon,
runware: RunwareIcon,
ranoai: RanoAIIcon,
} as const;

// Type for provider icon keys
Expand Down Expand Up @@ -1637,6 +1653,7 @@ export const providerLogoUrls: Partial<
gonka24: ProviderIcons.gonka24,
fireworks: ProviderIcons.fireworks,
runware: ProviderIcons.runware,
ranoai: ProviderIcons.ranoai,
};

export const getProviderLogoDarkModeClasses = () => {
Expand Down
Loading