diff --git a/CHANGELOG.md b/CHANGELOG.md index c3c6459c5f..62ab432f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai - Cron: `cron run` defaults to force execution; use `--due` to restrict to due-only. (#10776) Thanks @tyler6204. - Models: support Anthropic Opus 4.6 and OpenAI Codex gpt-5.3-codex (forward-compat fallbacks). (#9853, #10720, #9995) Thanks @TinyTb, @calvin-hpnet, @tyler6204. - Providers: add xAI (Grok) support. (#9885) Thanks @grp06. +- Providers: add Baidu Qianfan support. (#8868) Thanks @ide-rea. - Web UI: add token usage dashboard. (#10072) Thanks @Takhoffman. - Memory: native Voyage AI support. (#7078) Thanks @mcinteerj. - Sessions: cap sessions_history payloads to reduce context overflow. (#10000) Thanks @gut-puncture. diff --git a/docs/docs.json b/docs/docs.json index 48dcaa6ee5..7395ace49b 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -98,6 +98,10 @@ "source": "/opencode", "destination": "/providers/opencode" }, + { + "source": "/qianfan", + "destination": "/providers/qianfan" + }, { "source": "/mattermost", "destination": "/channels/mattermost" @@ -1006,7 +1010,8 @@ "providers/opencode", "providers/glm", "providers/zai", - "providers/synthetic" + "providers/synthetic", + "providers/qianfan" ] } ] diff --git a/docs/providers/index.md b/docs/providers/index.md index 7bdf660134..4c60fea4c7 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -50,6 +50,7 @@ See [Venice AI](/providers/venice). - [MiniMax](/providers/minimax) - [Venice (Venice AI, privacy-focused)](/providers/venice) - [Ollama (local models)](/providers/ollama) +- [Qianfan](/providers/qianfan) ## Transcription providers diff --git a/docs/providers/models.md b/docs/providers/models.md index b5dcf11f06..1a9d28cf8a 100644 --- a/docs/providers/models.md +++ b/docs/providers/models.md @@ -46,6 +46,7 @@ See [Venice AI](/providers/venice). - [MiniMax](/providers/minimax) - [Venice (Venice AI)](/providers/venice) - [Amazon Bedrock](/bedrock) +- [Qianfan](/providers/qianfan) For the full provider catalog (xAI, Groq, Mistral, etc.) and advanced configuration, see [Model providers](/concepts/model-providers). diff --git a/docs/providers/qianfan.md b/docs/providers/qianfan.md new file mode 100644 index 0000000000..9991fec8de --- /dev/null +++ b/docs/providers/qianfan.md @@ -0,0 +1,38 @@ +--- +summary: "Use Qianfan's unified API to access many models in OpenClaw" +read_when: + - You want a single API key for many LLMs + - You need Baidu Qianfan setup guidance +title: "Qianfan" +--- + +# Qianfan Provider Guide + +Qianfan is Baidu's MaaS platform, provides a **unified API** that routes requests to many models behind a single +endpoint and API key. It is OpenAI-compatible, so most OpenAI SDKs work by switching the base URL. + +## Prerequisites + +1. A Baidu Cloud account with Qianfan API access +2. An API key from the Qianfan console +3. OpenClaw installed on your system + +## Getting Your API Key + +1. Visit the [Qianfan Console](https://console.bce.baidu.com/qianfan/ais/console/apiKey) +2. Create a new application or select an existing one +3. Generate an API key (format: `bce-v3/ALTAK-...`) +4. Copy the API key for use with OpenClaw + +## CLI setup + +```bash +openclaw onboard --auth-choice qianfan-api-key +``` + +## Related Documentation + +- [OpenClaw Configuration](/configuration) +- [Model Providers](/concepts/model-providers) +- [Agent Setup](/agents) +- [Qianfan API Documentation](https://cloud.baidu.com/doc/qianfan-api/s/3m7of64lb) diff --git a/extensions/memory-lancedb/index.ts b/extensions/memory-lancedb/index.ts index 5e4def80fa..efbc8a8fb5 100644 --- a/extensions/memory-lancedb/index.ts +++ b/extensions/memory-lancedb/index.ts @@ -6,8 +6,8 @@ * Provides seamless auto-recall and auto-capture via lifecycle hooks. */ +import type * as LanceDB from "@lancedb/lancedb"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; -import * as lancedb from "@lancedb/lancedb"; import { Type } from "@sinclair/typebox"; import { randomUUID } from "node:crypto"; import OpenAI from "openai"; @@ -23,6 +23,19 @@ import { // Types // ============================================================================ +let lancedbImportPromise: Promise | null = null; +const loadLanceDB = async (): Promise => { + if (!lancedbImportPromise) { + lancedbImportPromise = import("@lancedb/lancedb"); + } + try { + return await lancedbImportPromise; + } catch (err) { + // Common on macOS today: upstream package may not ship darwin native bindings. + throw new Error(`memory-lancedb: failed to load LanceDB. ${String(err)}`); + } +}; + type MemoryEntry = { id: string; text: string; @@ -44,8 +57,8 @@ type MemorySearchResult = { const TABLE_NAME = "memories"; class MemoryDB { - private db: lancedb.Connection | null = null; - private table: lancedb.Table | null = null; + private db: LanceDB.Connection | null = null; + private table: LanceDB.Table | null = null; private initPromise: Promise | null = null; constructor( @@ -66,6 +79,7 @@ class MemoryDB { } private async doInitialize(): Promise { + const lancedb = await loadLanceDB(); this.db = await lancedb.connect(this.dbPath); const tables = await this.db.tableNames(); diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index f6d669aa8f..807655b52d 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -257,6 +257,30 @@ describe("getApiKeyForModel", () => { } }); + it("resolves Qianfan API key from env", async () => { + const previous = process.env.QIANFAN_API_KEY; + + try { + process.env.QIANFAN_API_KEY = "qianfan-test-key"; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "qianfan", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe("qianfan-test-key"); + expect(resolved.source).toContain("QIANFAN_API_KEY"); + } finally { + if (previous === undefined) { + delete process.env.QIANFAN_API_KEY; + } else { + process.env.QIANFAN_API_KEY = previous; + } + } + }); + it("resolves Vercel AI Gateway API key from env", async () => { const previousGatewayKey = process.env.AI_GATEWAY_API_KEY; diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 324547c1d3..35e33fbf40 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -302,6 +302,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", + qianfan: "QIANFAN_API_KEY", ollama: "OLLAMA_API_KEY", }; const envVar = envMap[normalized]; diff --git a/src/agents/models-config.providers.qianfan.test.ts b/src/agents/models-config.providers.qianfan.test.ts new file mode 100644 index 0000000000..1752726289 --- /dev/null +++ b/src/agents/models-config.providers.qianfan.test.ts @@ -0,0 +1,25 @@ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { resolveImplicitProviders } from "./models-config.providers.js"; + +describe("Qianfan provider", () => { + it("should include qianfan when QIANFAN_API_KEY is configured", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + const previous = process.env.QIANFAN_API_KEY; + process.env.QIANFAN_API_KEY = "test-key"; + + try { + const providers = await resolveImplicitProviders({ agentDir }); + expect(providers?.qianfan).toBeDefined(); + expect(providers?.qianfan?.apiKey).toBe("QIANFAN_API_KEY"); + } finally { + if (previous === undefined) { + delete process.env.QIANFAN_API_KEY; + } else { + process.env.QIANFAN_API_KEY = previous; + } + } + }); +}); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index ddfcd7e641..d4ae66cc03 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -80,6 +80,17 @@ const OLLAMA_DEFAULT_COST = { cacheWrite: 0, }; +export const QIANFAN_BASE_URL = "https://qianfan.baidubce.com/v2"; +export const QIANFAN_DEFAULT_MODEL_ID = "deepseek-v3.2"; +const QIANFAN_DEFAULT_CONTEXT_WINDOW = 98304; +const QIANFAN_DEFAULT_MAX_TOKENS = 32768; +const QIANFAN_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + interface OllamaModel { name: string; modified_at: string; @@ -403,6 +414,33 @@ async function buildOllamaProvider(): Promise { }; } +export function buildQianfanProvider(): ProviderConfig { + return { + baseUrl: QIANFAN_BASE_URL, + api: "openai-completions", + models: [ + { + id: QIANFAN_DEFAULT_MODEL_ID, + name: "DEEPSEEK V3.2", + reasoning: true, + input: ["text"], + cost: QIANFAN_DEFAULT_COST, + contextWindow: QIANFAN_DEFAULT_CONTEXT_WINDOW, + maxTokens: QIANFAN_DEFAULT_MAX_TOKENS, + }, + { + id: "ernie-5.0-thinking-preview", + name: "ERNIE-5.0-Thinking-Preview", + reasoning: true, + input: ["text", "image"], + cost: QIANFAN_DEFAULT_COST, + contextWindow: 119000, + maxTokens: 64000, + }, + ], + }; +} + export async function resolveImplicitProviders(params: { agentDir: string; }): Promise { @@ -498,6 +536,13 @@ export async function resolveImplicitProviders(params: { providers.ollama = { ...(await buildOllamaProvider()), apiKey: ollamaKey }; } + const qianfanKey = + resolveEnvApiKeyVarName("qianfan") ?? + resolveApiKeyFromProfiles({ provider: "qianfan", store: authStore }); + if (qianfanKey) { + providers.qianfan = { ...buildQianfanProvider(), apiKey: qianfanKey }; + } + return providers; } diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 5eb784997b..0ddaeb55e7 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -58,7 +58,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|token|chutes|openai-codex|openai-api-key|xai-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|token|chutes|openai-codex|openai-api-key|xai-api-key|qianfan-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", @@ -87,6 +87,7 @@ export function registerOnboardCommand(program: Command) { .option("--venice-api-key ", "Venice API key") .option("--opencode-zen-api-key ", "OpenCode Zen API key") .option("--xai-api-key ", "xAI API key") + .option("--qianfan-api-key ", "QIANFAN API key") .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|tailnet|lan|auto|custom") .option("--gateway-auth ", "Gateway auth: token|password") @@ -137,6 +138,7 @@ export function registerOnboardCommand(program: Command) { geminiApiKey: opts.geminiApiKey as string | undefined, zaiApiKey: opts.zaiApiKey as string | undefined, xiaomiApiKey: opts.xiaomiApiKey as string | undefined, + qianfanApiKey: opts.qianfanApiKey as string | undefined, minimaxApiKey: opts.minimaxApiKey as string | undefined, syntheticApiKey: opts.syntheticApiKey as string | undefined, veniceApiKey: opts.veniceApiKey as string | undefined, diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 3b935f258b..905c2377fb 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -23,6 +23,7 @@ export type AuthChoiceGroupId = | "synthetic" | "venice" | "qwen" + | "qianfan" | "xai"; export type AuthChoiceGroup = { @@ -38,6 +39,18 @@ const AUTH_CHOICE_GROUP_DEFS: { hint?: string; choices: AuthChoice[]; }[] = [ + { + value: "xai", + label: "xAI (Grok)", + hint: "API key", + choices: ["xai-api-key"], + }, + { + value: "qianfan", + label: "Qianfan", + hint: "API key", + choices: ["qianfan-api-key"], + }, { value: "openai", label: "OpenAI", @@ -156,6 +169,10 @@ export function buildAuthChoiceOptions(params: { options.push({ value: "chutes", label: "Chutes (OAuth)" }); options.push({ value: "openai-api-key", label: "OpenAI API key" }); options.push({ value: "xai-api-key", label: "xAI (Grok) API key" }); + options.push({ + value: "qianfan-api-key", + label: "Qianfan API key", + }); options.push({ value: "openrouter-api-key", label: "OpenRouter API key" }); options.push({ value: "ai-gateway-api-key", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 1da45a49bd..574128d6ac 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -15,6 +15,8 @@ import { applyAuthProfileConfig, applyCloudflareAiGatewayConfig, applyCloudflareAiGatewayProviderConfig, + applyQianfanConfig, + applyQianfanProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -35,6 +37,7 @@ import { applyXiaomiProviderConfig, applyZaiConfig, CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, + QIANFAN_DEFAULT_MODEL_REF, KIMI_CODING_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, @@ -43,6 +46,7 @@ import { VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, setCloudflareAiGatewayConfig, + setQianfanApiKey, setGeminiApiKey, setKimiCodingApiKey, setMoonshotApiKey, @@ -104,6 +108,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "venice-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; + } else if (params.opts.tokenProvider === "qianfan") { + authChoice = "qianfan-api-key"; } } @@ -797,5 +803,61 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "qianfan-api-key") { + let hasCredential = false; + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "qianfan") { + setQianfanApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Get your API key at: https://console.bce.baidu.com/qianfan/ais/console/apiKey", + "API key format: bce-v3/ALTAK-...", + ].join("\n"), + "QIANFAN", + ); + } + const envKey = resolveEnvApiKey("qianfan"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing QIANFAN_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + setQianfanApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter QIANFAN API key", + validate: validateApiKeyInput, + }); + setQianfanApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "qianfan:default", + provider: "qianfan", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: QIANFAN_DEFAULT_MODEL_REF, + applyDefaultConfig: applyQianfanConfig, + applyProviderConfig: applyQianfanProviderConfig, + noteDefault: QIANFAN_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + return null; } diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index e78f5ed270..c77283b507 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -33,6 +33,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "xai-api-key": "xai", "qwen-portal": "qwen-portal", "minimax-portal": "minimax-portal", + "qianfan-api-key": "qianfan", }; export function resolvePreferredProviderForAuthChoice(choice: AuthChoice): string | undefined { diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 13299915d6..131067fc95 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -1,9 +1,15 @@ import type { OpenClawConfig } from "../config/config.js"; +import type { ModelApi } from "../config/types.models.js"; import { buildCloudflareAiGatewayModelDefinition, resolveCloudflareAiGatewayBaseUrl, } from "../agents/cloudflare-ai-gateway.js"; -import { buildXiaomiProvider, XIAOMI_DEFAULT_MODEL_ID } from "../agents/models-config.providers.js"; +import { + buildQianfanProvider, + buildXiaomiProvider, + QIANFAN_DEFAULT_MODEL_ID, + XIAOMI_DEFAULT_MODEL_ID, +} from "../agents/models-config.providers.js"; import { buildSyntheticModelDefinition, SYNTHETIC_BASE_URL, @@ -27,6 +33,8 @@ import { import { buildMoonshotModelDefinition, buildXaiModelDefinition, + QIANFAN_BASE_URL, + QIANFAN_DEFAULT_MODEL_REF, KIMI_CODING_MODEL_REF, MOONSHOT_BASE_URL, MOONSHOT_CN_BASE_URL, @@ -705,3 +713,80 @@ export function applyAuthProfileConfig( }, }; } + +export function applyQianfanProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[QIANFAN_DEFAULT_MODEL_REF] = { + ...models[QIANFAN_DEFAULT_MODEL_REF], + alias: models[QIANFAN_DEFAULT_MODEL_REF]?.alias ?? "QIANFAN", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.qianfan; + const defaultProvider = buildQianfanProvider(); + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const defaultModels = defaultProvider.models ?? []; + const hasDefaultModel = existingModels.some((model) => model.id === QIANFAN_DEFAULT_MODEL_ID); + const mergedModels = + existingModels.length > 0 + ? hasDefaultModel + ? existingModels + : [...existingModels, ...defaultModels] + : defaultModels; + const { + apiKey: existingApiKey, + baseUrl: existingBaseUrl, + api: existingApi, + ...existingProviderRest + } = (existingProvider ?? {}) as Record as { + apiKey?: string; + baseUrl?: string; + api?: ModelApi; + }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.qianfan = { + ...existingProviderRest, + baseUrl: existingBaseUrl ?? QIANFAN_BASE_URL, + api: existingApi ?? "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : defaultProvider.models, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyQianfanConfig(cfg: OpenClawConfig): OpenClawConfig { + const next = applyQianfanProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: QIANFAN_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index c8992efff5..20784d34d0 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -205,6 +205,18 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { }); } +export function setQianfanApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "qianfan:default", + credential: { + type: "api_key", + provider: "qianfan", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + export function setXaiApiKey(key: string, agentDir?: string) { upsertAuthProfile({ profileId: "xai:default", diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index 3873a877c6..611a7cb8ea 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -1,4 +1,5 @@ import type { ModelDefinitionConfig } from "../config/types.js"; +import { QIANFAN_BASE_URL, QIANFAN_DEFAULT_MODEL_ID } from "../agents/models-config.providers.js"; export const DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1"; export const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic"; @@ -16,6 +17,9 @@ export const MOONSHOT_DEFAULT_MAX_TOKENS = 8192; export const KIMI_CODING_MODEL_ID = "k2p5"; export const KIMI_CODING_MODEL_REF = `kimi-coding/${KIMI_CODING_MODEL_ID}`; +export { QIANFAN_BASE_URL, QIANFAN_DEFAULT_MODEL_ID }; +export const QIANFAN_DEFAULT_MODEL_REF = `qianfan/${QIANFAN_DEFAULT_MODEL_ID}`; + // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. export const MINIMAX_API_COST = { input: 15, diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 982570d0d2..a2732b7bfa 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -7,6 +7,8 @@ export { applyAuthProfileConfig, applyCloudflareAiGatewayConfig, applyCloudflareAiGatewayProviderConfig, + applyQianfanConfig, + applyQianfanProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -45,6 +47,7 @@ export { OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, setCloudflareAiGatewayConfig, + setQianfanApiKey, setGeminiApiKey, setKimiCodingApiKey, setMinimaxApiKey, @@ -69,6 +72,9 @@ export { buildMoonshotModelDefinition, DEFAULT_MINIMAX_BASE_URL, MOONSHOT_CN_BASE_URL, + QIANFAN_BASE_URL, + QIANFAN_DEFAULT_MODEL_ID, + QIANFAN_DEFAULT_MODEL_REF, KIMI_CODING_MODEL_ID, KIMI_CODING_MODEL_REF, MINIMAX_API_BASE_URL, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index adc20f1c39..c1c87812de 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -11,6 +11,7 @@ import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default import { applyAuthProfileConfig, applyCloudflareAiGatewayConfig, + applyQianfanConfig, applyKimiCodeConfig, applyMinimaxApiConfig, applyMinimaxConfig, @@ -26,6 +27,7 @@ import { applyZaiConfig, setAnthropicApiKey, setCloudflareAiGatewayConfig, + setQianfanApiKey, setGeminiApiKey, setKimiCodingApiKey, setMinimaxApiKey, @@ -243,6 +245,29 @@ export async function applyNonInteractiveAuthChoice(params: { return applyXaiConfig(nextConfig); } + if (authChoice === "qianfan-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "qianfan", + cfg: baseConfig, + flagValue: opts.qianfanApiKey, + flagName: "--qianfan-api-key", + envVar: "QIANFAN_API_KEY", + runtime, + }); + if (!resolved) { + return null; + } + if (resolved.source !== "profile") { + setQianfanApiKey(resolved.key); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "qianfan:default", + provider: "qianfan", + mode: "api_key", + }); + return applyQianfanConfig(nextConfig); + } + if (authChoice === "openai-api-key") { const resolved = await resolveNonInteractiveApiKey({ provider: "openai", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index c64d37046c..488a4e9f5a 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -36,6 +36,7 @@ export type AuthChoice = | "copilot-proxy" | "qwen-portal" | "xai-api-key" + | "qianfan-api-key" | "skip"; export type GatewayAuthChoice = "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full"; @@ -81,6 +82,7 @@ export type OnboardOptions = { veniceApiKey?: string; opencodeZenApiKey?: string; xaiApiKey?: string; + qianfanApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind; gatewayAuth?: GatewayAuthChoice; diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index a0c154d84b..9295b1d139 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -121,7 +121,8 @@ const saveSessionToMemory: HookHandler = async (event) => { messageCount, }); - if (sessionContent && cfg) { + // Avoid calling the model provider in unit tests, keep hooks fast and deterministic. + if (sessionContent && cfg && !process.env.VITEST && process.env.NODE_ENV !== "test") { log.debug("Calling generateSlugViaLLM..."); // Dynamically import the LLM slug generator (avoids module caching issues) // When compiled, handler is at dist/hooks/bundled/session-memory/handler.js