Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -246,6 +246,9 @@ LLM_FIREWORKS_BASE_URL=https://api.fireworks.ai/inference
# RanoAI
LLM_RANOAI_API_KEY=your_ranoai_key_here

# Baidu (Qianfan)
LLM_BAIDU_API_KEY=your_baidu_qianfan_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 @@ -99,6 +99,9 @@ LLM_SCX_AI_API_KEY=your_scx_ai_key_here
# RanoAI
LLM_RANOAI_API_KEY=your_ranoai_key_here

# Baidu (Qianfan)
LLM_BAIDU_API_KEY=your_baidu_qianfan_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 @@ -145,6 +145,7 @@ jobs:
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 }}
LLM_BAIDU_API_KEY: ${{ secrets.LLM_BAIDU_API_KEY }}

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

it("should not double-bill Baidu reasoning tokens", async () => {
// Qianfan reports reasoning in completion_tokens_details while already
// counting it inside completion_tokens: a thinking-only reply comes back
// as completion_tokens === reasoning_tokens, so adding it again would
// double the billed output.
const result = await calculateCosts(
"ernie-5.0",
"baidu",
null,
1000,
400, // completionTokens already includes the 400 reasoning tokens
null,
undefined,
400,
);

// inputPrice 1.4e-6, outputPrice 5.6e-6.
expect(result.inputCost).toBeCloseTo(0.0014, 10);
expect(result.outputCost).toBeCloseTo(0.00224, 10); // 400 * 5.6e-6, not 800
expect(result.completionTokens).toBe(400);
});

it("should handle null reasoning tokens gracefully", async () => {
const result = await calculateCosts(
"gemini-2.5-pro",
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 @@ -738,8 +738,9 @@ export async function calculateCosts(
// 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.
// billed output on reasoning requests. Baidu's Qianfan reports the same way
// (a thinking-only reply returns completion_tokens === reasoning_tokens).
// For remaining providers, add reasoning separately.
const completionIncludesReasoning =
provider === "google-ai-studio" ||
provider === "glacier" ||
Expand All @@ -751,6 +752,7 @@ export async function calculateCosts(
provider === "sakana" ||
provider === "meta" ||
provider === "ranoai" ||
provider === "baidu" ||
provider === "aws-mantle";
const totalOutputTokens = completionIncludesReasoning
? calculatedCompletionTokens
Expand Down
17 changes: 9 additions & 8 deletions apps/gateway/src/models/models.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,15 @@ describe("Models API", () => {
);
}

// deepseek-v3.2 has a deactivated Nebius mapping declaring 32768; the
// advertised bound must come from the mappings that can still serve
// (65536), not from limits that no longer apply.
const deepseek = json.data.find(
(m: { id: string }) => m.id === "deepseek-v3.2",
);
expect(deepseek).toBeDefined();
expect(deepseek.max_output).toBe(65536);
// kimi-k2 has a deactivated mapping declaring 8192; the advertised bound
// must come from the mappings that can still serve (131072), not from
// limits that no longer apply. deepseek-v3.2 used to pin this case, but
// Qianfan serves it with a genuine 32768 output cap, which matches the
// deactivated Nebius limit and left the assertion unable to tell the two
// apart.
const kimi = json.data.find((m: { id: string }) => m.id === "kimi-k2");
expect(kimi).toBeDefined();
expect(kimi.max_output).toBe(131072);
});

test("GET /v1/models exposes reasoning_efforts on provider mappings that define them", async () => {
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 @@ -48,6 +48,7 @@ export const providerLogoUrls: Partial<
fireworks: ProviderIcons.fireworks,
runware: ProviderIcons.runware,
ranoai: ProviderIcons.ranoai,
baidu: ProviderIcons.baidu,
};

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 @@ -466,6 +466,7 @@ llmProviders:
# LLM_TOGETHER_AI_API_KEY: ""
# LLM_SCX_AI_API_KEY: ""
# LLM_RANOAI_API_KEY: ""
# LLM_BAIDU_API_KEY: ""
# LLM_ALIBABA_API_KEY: ""
# LLM_NEBIUS_API_KEY: ""
# LLM_CEREBRAS_API_KEY: ""
Expand Down
4 changes: 4 additions & 0 deletions packages/actions/src/get-provider-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const PROVIDER_DEFAULT_BASE_URLS: Partial<Record<ProviderId, string>> = {
gonka24: "https://api.gonka24.com",
fireworks: "https://api.fireworks.ai/inference",
ranoai: "https://api.ranoai.com",
baidu: "https://qianfan.baidubce.com",
};

export function getProviderDefaultBaseUrl(
Expand Down Expand Up @@ -952,6 +953,9 @@ export function getProviderEndpoint(
}
return `${url}/v1/chat/completions`;
}
// Qianfan's OpenAI-compatible surface is versioned v2, not v1.
case "baidu":
return `${url}/v2/chat/completions`;
case "deepseek":
case "moonshot":
case "nebius":
Expand Down
2 changes: 2 additions & 0 deletions packages/models/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { alibabaModels } from "./models/alibaba.js";
import { anthropicModels } from "./models/anthropic.js";
import { atlascloudModels } from "./models/atlascloud.js";
import { baaiModels } from "./models/baai.js";
import { baiduModels } from "./models/baidu.js";
import { bytedanceModels } from "./models/bytedance.js";
import { deepseekModels } from "./models/deepseek.js";
import { elevenlabsModels } from "./models/elevenlabs.js";
Expand Down Expand Up @@ -767,6 +768,7 @@ export const models = [
...alibabaModels,
...atlascloudModels,
...baaiModels,
...baiduModels,
...bytedanceModels,
...nousresearchModels,
...reveModels,
Expand Down
31 changes: 31 additions & 0 deletions packages/models/src/models/baidu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ModelDefinition } from "@/models.js";

export const baiduModels = [
{
id: "ernie-5.0",
name: "ERNIE 5.0",
description:
"Baidu's flagship natively multimodal model with a 248k context window, accepting text, image, and video input and thinking before it answers.",
family: "baidu",
releasedAt: new Date("2026-03-05"),
providers: [
{
providerId: "baidu" as const,
externalId: "ernie-5.0",
inputPrice: "1.4e-6",
outputPrice: "5.6e-6",
requestPrice: "0",
contextSize: 248832,
maxOutput: 126976,
streaming: true,
reasoning: true,
vision: true,
tools: true,
// jsonOutput is left off across every Qianfan mapping until
// response_format is confirmed against a funded account: an
// unsupported claim 400s the request, while omitting it only
// keeps JSON-mode traffic on another provider.
},
Comment thread
steebchen marked this conversation as resolved.
Resolved
],
},
] as const satisfies ModelDefinition[];
41 changes: 41 additions & 0 deletions packages/models/src/models/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,19 @@ export const deepseekModels = [
tools: true,
jsonOutput: true,
},
{
providerId: "baidu",
externalId: "deepseek-v3.2",
inputPrice: "0.28e-6",
cachedInputPrice: "0.028e-6",
outputPrice: "0.42e-6",
requestPrice: "0",
contextSize: 131072,
maxOutput: 32768,
streaming: true,
vision: false,
tools: true,
},
],
},
{
Expand Down Expand Up @@ -487,6 +500,20 @@ export const deepseekModels = [
jsonOutput: true,
jsonOutputSchema: true,
},
{
providerId: "baidu",
externalId: "deepseek-v4-pro",
inputPrice: "1.69e-6",
cachedInputPrice: "0.14e-6",
outputPrice: "3.38e-6",
requestPrice: "0",
contextSize: 1441792,
maxOutput: 393216,
streaming: true,
reasoning: true,
vision: false,
tools: true,
},
],
},
{
Expand Down Expand Up @@ -774,6 +801,20 @@ export const deepseekModels = [
supportedToolChoices: ["auto", "none"],
jsonOutput: true,
},
{
providerId: "baidu",
externalId: "deepseek-v4-flash",
inputPrice: "0.14e-6",
cachedInputPrice: "0.028e-6",
outputPrice: "0.28e-6",
requestPrice: "0",
contextSize: 1441792,
maxOutput: 393216,
streaming: true,
reasoning: true,
vision: false,
tools: true,
},
],
},
] as const satisfies ModelDefinition[];
17 changes: 17 additions & 0 deletions packages/models/src/models/moonshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,23 @@ export const moonshotModels = [
// normalize it defensively in both modes.
healStreamingJsonOutput: true,
},
{
providerId: "baidu",
externalId: "kimi-k2.6",
// Mirrors the Moonshot mapping: the model rejects forced
// tool_choice while thinking is on.
supportedToolChoices: ["auto", "none"],
inputPrice: "0.95e-6",
cachedInputPrice: "0.16e-6",
outputPrice: "4e-6",
requestPrice: "0",
contextSize: 524288,
maxOutput: 262144,
streaming: true,
reasoning: true,
vision: true,
tools: true,
},
],
},
{
Expand Down
42 changes: 42 additions & 0 deletions packages/models/src/models/zai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,20 @@ export const zaiModels = [
tools: true,
jsonOutput: true,
},
{
providerId: "baidu",
externalId: "glm-5.2",
inputPrice: "1.4e-6",
cachedInputPrice: "0.26e-6",
outputPrice: "4.4e-6",
requestPrice: "0",
contextSize: 1179648,
maxOutput: 131072,
streaming: true,
reasoning: true,
vision: false,
tools: true,
},
],
},
{
Expand Down Expand Up @@ -350,6 +364,20 @@ export const zaiModels = [
tools: true,
jsonOutput: true,
},
{
providerId: "baidu",
externalId: "glm-5.1",
inputPrice: "1.4e-6",
cachedInputPrice: "0.26e-6",
outputPrice: "4.4e-6",
requestPrice: "0",
contextSize: 333824,
maxOutput: 131072,
streaming: true,
reasoning: true,
vision: false,
tools: true,
},
],
},
{
Expand Down Expand Up @@ -506,6 +534,20 @@ export const zaiModels = [
tools: true,
jsonOutput: true,
},
{
providerId: "baidu",
externalId: "glm-5",
inputPrice: "1e-6",
cachedInputPrice: "0.2e-6",
outputPrice: "3.2e-6",
requestPrice: "0",
contextSize: 335872,
maxOutput: 131072,
streaming: true,
reasoning: true,
vision: false,
tools: true,
},
],
},
{
Expand Down
29 changes: 29 additions & 0 deletions packages/models/src/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,35 @@ export const providers: ProviderDefinition[] = [
},
priority: 1.2,
},
{
id: "baidu",
name: "Baidu",
description:
"Baidu's Qianfan platform serving ERNIE alongside DeepSeek, GLM, and Kimi models",
env: {
required: {
apiKey: "LLM_BAIDU_API_KEY",
},
},
streaming: true,
cancellation: true,
color: "#2932E1",
website: "https://cloud.baidu.com/product/qianfan",
statusPageUrl: null,
announcement: null,
termsUrl: "https://cloud.baidu.com/doc/Agreements/s/yjwvy1x03",
privacyPolicyUrl: "https://cloud.baidu.com/doc/Agreements/s/Plr0fi68q",
headquarters: "CN",
// Qianfan publishes no API training / prompt logging commitment we can
// point at, so every attribute stays unknown and fails closed under a
// compliance policy rather than claiming a guarantee Baidu never made.
dataPolicy: {
apiTraining: null,
consumerTraining: null,
promptLogging: null,
retentionPeriod: null,
},
},
{
id: "perplexity",
name: "Perplexity",
Expand Down
Loading
Loading