Compare commits

...

9 Commits

Author SHA1 Message Date
celestial-vault eb9cdd33b2 remove console logs 2025-11-04 10:38:04 -08:00
celestial-vault c74dcbbe2f translate chinese comments 2025-11-04 10:33:11 -08:00
zhaochenxue e69a5284fa Update providerUtils.ts 2025-11-04 20:01:53 +08:00
zhaochenxue c472eea632 fix: bot 2025-11-04 19:56:29 +08:00
zhaochenxue 40be38c8d4 Delete AIHUBMIX_INTEGRATION.md 2025-11-04 19:44:31 +08:00
zhaochenxue 3908daafa9 merge 2025-11-04 19:42:05 +08:00
zhaochenxue aa4f6d3192 fix: add plan/act mode fields for AIhubmix and Hicap providers
- Add planModeAihubmixModelId and planModeAihubmixModelInfo
- Add actModeAihubmixModelId and actModeAihubmixModelInfo
- Add planModeHicapModelId and planModeHicapModelInfo
- Add actModeHicapModelId and actModeHicapModelInfo
- Ensures model selection works correctly in both plan and act modes
2025-11-04 15:13:50 +08:00
zhaochenxue 8a25abae7a docs: add AIhubmix integration documentation 2025-11-04 15:02:55 +08:00
zhaochenxue d8999415e4 feat: add AIhubmix provider integration
- Add AIhubmix as a new provider with full API integration
- Implement AIhubmixHandler for API interactions
- Add model fetching functionality via getAihubmixModels
- Create UI components for AIhubmix configuration
- Update proto definitions and API configuration
- Add changeset for version tracking
2025-11-04 14:48:49 +08:00
16 changed files with 692 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add new provider AIhubmix
+17
View File
@@ -45,6 +45,8 @@ service ModelsService {
rpc getSapAiCoreModels(SapAiCoreModelsRequest) returns (SapAiCoreModelsResponse);
// Fetches available models from OCA
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
// Fetches available models from AIhubmix
rpc getAihubmixModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -172,6 +174,7 @@ message ModelsApiSecrets {
optional string oca_api_key = 36;
optional string oca_refresh_token = 37;
optional string minimax_api_key = 38;
optional string aihubmix_api_key = 39;
}
// API configuration options (non-secret settings)
@@ -218,6 +221,8 @@ message ModelsApiOptions {
optional string oca_mode = 39;
optional bool aws_use_global_inference = 40;
optional string minimax_api_line = 41;
optional string aihubmix_base_url = 42;
optional string aihubmix_app_code = 43;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
@@ -253,6 +258,8 @@ message ModelsApiOptions {
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
optional string plan_mode_oca_model_id = 131;
optional OcaModelInfo plan_mode_oca_model_info = 132;
optional string plan_mode_aihubmix_model_id = 133;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 134;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -288,6 +295,8 @@ message ModelsApiOptions {
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
optional string act_mode_oca_model_id = 231;
optional OcaModelInfo act_mode_oca_model_info = 232;
optional string act_mode_aihubmix_model_id = 233;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 234;
}
// Request for updating API configuration (legacy - uses combined configuration)
@@ -413,6 +422,7 @@ enum ApiProvider {
OCA = 35;
MINIMAX = 36;
HICAP = 37;
AIHUBMIX = 38;
}
// Model info for OpenAI-compatible models
@@ -534,6 +544,9 @@ message ModelsApiConfiguration {
optional string minimax_api_line = 79;
optional string hicap_model_id = 80;
optional string hicap_api_key = 81;
optional string aihubmix_api_key = 82;
optional string aihubmix_base_url = 83;
optional string aihubmix_app_code = 84;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
@@ -571,6 +584,8 @@ message ModelsApiConfiguration {
optional OcaModelInfo plan_mode_oca_model_info = 132;
optional string plan_mode_hicap_model_id = 133;
optional OpenRouterModelInfo plan_mode_hicap_model_info = 134;
optional string plan_mode_aihubmix_model_id = 135;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 136;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -608,4 +623,6 @@ message ModelsApiConfiguration {
optional OcaModelInfo act_mode_oca_model_info = 232;
optional string act_mode_hicap_model_id = 233;
optional OpenRouterModelInfo act_mode_hicap_model_info = 234;
optional string act_mode_aihubmix_model_id = 235;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 236;
}
+7
View File
@@ -215,6 +215,13 @@ message Settings {
optional int32 max_consecutive_mistakes = 124;
optional bool subagents_enabled = 125;
optional int32 subagent_terminal_output_line_limit = 126;
optional string aihubmix_api_key = 127;
optional string aihubmix_base_url = 128;
optional string aihubmix_app_code = 129;
optional string plan_mode_aihubmix_model_id = 130;
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
optional string act_mode_aihubmix_model_id = 132;
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
}
message DictationSettings {
+11
View File
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { ClineTool } from "@/shared/tools"
import { AIhubmixHandler } from "./providers/aihubmix"
import { AnthropicHandler } from "./providers/anthropic"
import { AskSageHandler } from "./providers/asksage"
import { BasetenHandler } from "./providers/baseten"
@@ -392,6 +393,16 @@ function createHandlerForProvider(
: options.actModeOcaModelInfo?.supportsPromptCache,
taskId: options.ulid,
})
case "aihubmix":
return new AIhubmixHandler({
onRetryAttempt: options.onRetryAttempt,
apiKey: options.aihubmixApiKey,
baseURL: options.aihubmixBaseUrl,
appCode: options.aihubmixAppCode,
modelId: mode === "plan" ? (options as any).planModeAihubmixModelId : (options as any).actModeAihubmixModelId,
modelInfo:
mode === "plan" ? (options as any).planModeAihubmixModelInfo : (options as any).actModeAihubmixModelInfo,
})
case "minimax":
return new MinimaxHandler({
onRetryAttempt: options.onRetryAttempt,
+331
View File
@@ -0,0 +1,331 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { GenerateContentConfig, GoogleGenAI } from "@google/genai"
import { ModelInfo } from "@shared/api"
import OpenAI from "openai"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface AIhubmixHandlerOptions extends CommonApiHandlerOptions {
apiKey?: string
baseURL?: string
appCode?: string
modelId?: string
modelInfo?: ModelInfo
thinkingBudgetTokens?: number
}
export class AIhubmixHandler implements ApiHandler {
private options: AIhubmixHandlerOptions
private anthropicClient: Anthropic | undefined
private openaiClient: OpenAI | undefined
private geminiClient: GoogleGenAI | undefined
constructor(options: AIhubmixHandlerOptions) {
const { baseURL, appCode, ...rest } = options
this.options = {
baseURL: baseURL ?? "https://aihubmix.com",
appCode: appCode ?? "KUWF9311", // Application code for discount
...rest,
}
}
private ensureAnthropicClient(): Anthropic {
if (!this.anthropicClient) {
if (!this.options.apiKey) {
throw new Error("AIhubmix API key is required")
}
try {
this.anthropicClient = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.baseURL,
defaultHeaders: {
"APP-Code": this.options.appCode,
},
})
} catch (error) {
throw new Error(`Error creating Anthropic client: ${error.message}`)
}
}
return this.anthropicClient
}
private ensureOpenaiClient(): OpenAI {
if (!this.openaiClient) {
if (!this.options.apiKey) {
throw new Error("AIhubmix API key is required")
}
try {
this.openaiClient = new OpenAI({
apiKey: this.options.apiKey,
baseURL: `${this.options.baseURL}/v1`,
defaultHeaders: {
"APP-Code": this.options.appCode,
},
})
} catch (error) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.openaiClient
}
private ensureGeminiClient(): GoogleGenAI {
if (!this.geminiClient) {
if (!this.options.apiKey) {
throw new Error("AIhubmix API key is required")
}
try {
this.geminiClient = new GoogleGenAI({
apiKey: this.options.apiKey,
httpOptions: {
// AIhubmix Gemini compatible gateway, following Google GenAI path specification
baseUrl: `${this.options.baseURL}/gemini`,
headers: {
// @ts-expect-error
"APP-Code": this.options.appCode,
Authorization: `Bearer ${this.options.apiKey ?? ""}`,
},
},
})
} catch (error) {
throw new Error(`Error creating Gemini client: ${error.message}`)
}
}
return this.geminiClient
}
/**
* Routes to the corresponding client based on model name
*/
private routeModel(modelName: string): "anthropic" | "openai" | "gemini" | "openai-response" {
const id = modelName || ""
if (id.startsWith("claude")) {
return "anthropic"
}
if (id.startsWith("gemini") && !id.endsWith("-nothink") && !id.endsWith("-search")) {
return "gemini"
}
if (id === "gpt-5-pro" || id === "gpt-5-codex") {
return "openai-response"
}
return "openai"
}
/**
* Fixes tool_choice issue when tools array is empty
*/
private fixToolChoice(requestBody: any): any {
if (requestBody.tools?.length === 0 && requestBody.tool_choice) {
delete requestBody.tool_choice
}
return requestBody
}
@withRetry()
async *createMessage(systemPrompt: string, messages: any[]): ApiStream {
const modelId = this.options.modelId || ""
const route = this.routeModel(modelId)
switch (route) {
case "anthropic":
yield* this.createAnthropicMessage(systemPrompt, messages)
break
case "gemini":
yield* this.createGeminiMessage(systemPrompt, messages)
break
case "openai-response":
yield* this.createOpenaiResponseMessage(systemPrompt, messages)
break
case "openai":
yield* this.createOpenaiMessage(systemPrompt, messages)
break
default:
throw new Error(`Unsupported model route: ${route}`)
}
}
private async *createAnthropicMessage(systemPrompt: string, messages: any[]): ApiStream {
const client = this.ensureAnthropicClient()
const modelId = this.options.modelId || "claude-3-5-sonnet-20241022"
const stream = await client.messages.create({
model: modelId,
temperature: 0,
max_tokens: this.options.modelInfo?.maxTokens || 8192,
system: [{ text: systemPrompt, type: "text" }],
messages,
stream: true,
})
for await (const chunk of stream) {
switch (chunk?.type) {
case "message_start":
const usage = chunk.message.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
case "message_delta":
yield {
type: "usage",
inputTokens: 0,
outputTokens: chunk.usage.output_tokens || 0,
}
break
case "content_block_start":
if (chunk.content_block.type === "text") {
yield {
type: "text",
text: chunk.content_block.text,
}
}
break
case "content_block_delta":
if (chunk.delta.type === "text_delta") {
yield {
type: "text",
text: chunk.delta.text,
}
}
break
}
}
}
private async *createOpenaiResponseMessage(systemPrompt: string, messages: any[]): ApiStream {
const client = this.ensureOpenaiClient()
const modelId = this.options.modelId || "gpt-4o-mini"
// Convert Anthropic-style messages to Responses API input structure
const input = (messages || []).map((m: any) => {
const role = m.role || "user"
const contentArray = Array.isArray(m.content) ? m.content : [{ type: "text", text: m.content }]
const content = contentArray
.filter((c: any) => c != null)
.map((c: any) => {
// Image
if (c.type === "image" || c.type === "input_image" || c.type === "image_url") {
return { type: "input_image", image_url: c.image_url || c.url || c.source?.url }
}
// Text (user -> input_text, assistant -> output_text)
const text = c.text ?? (typeof c === "string" ? c : "")
return { type: role === "assistant" ? "output_text" : "input_text", text }
})
return { role, content }
})
// Use Responses streaming API with event-driven output
const stream = await (client as any).responses.stream({
model: modelId,
instructions: systemPrompt,
input,
})
for await (const event of stream as any) {
if (event?.type === "response.output_text.delta") {
yield { type: "text", text: event.delta || "" }
continue
}
if (event?.type === "response.completed") {
const usage = event.response?.usage || {}
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
}
continue
}
if (event?.type === "response.error") {
throw new Error(event.error?.message || "responses error")
}
}
}
private async *createOpenaiMessage(systemPrompt: string, messages: any[]): ApiStream {
const client = this.ensureOpenaiClient()
const modelId = this.options.modelId || "gpt-4o-mini"
const openaiMessages = [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
const requestBody = {
model: modelId,
messages: openaiMessages,
temperature: 0,
stream: true,
}
// Fix empty tools issue
const fixedRequestBody = this.fixToolChoice(requestBody)
const stream = await client.chat.completions.create(fixedRequestBody)
for await (const chunk of stream as any) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
private async *createGeminiMessage(systemPrompt: string, messages: any[]): ApiStream {
const client = this.ensureGeminiClient()
const modelId = this.options.modelId || "gemini-2.0-flash-exp"
const contents = messages.map(convertAnthropicMessageToGemini)
const requestConfig: GenerateContentConfig = {
systemInstruction: systemPrompt,
temperature: 0,
}
if (this.options.thinkingBudgetTokens) {
requestConfig.thinkingConfig = {
thinkingBudget: this.options.thinkingBudgetTokens,
includeThoughts: true,
}
}
const stream = await client.models.generateContentStream({
model: modelId,
contents,
config: requestConfig,
})
for await (const chunk of stream as any) {
if (chunk?.text) {
yield { type: "text", text: chunk.text }
}
}
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.modelId || "gpt-4o-mini",
info: this.options.modelInfo || {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
description: "AIhubmix unified model provider",
},
}
}
}
+4 -4
View File
@@ -594,7 +594,7 @@ class NewFileContentConstructor {
}
this.activateSearchState()
} else if (isSearchBlockEnd(line)) {
// 校验非标内容
// Validate non-standard content
if (!this.isSearchingActive()) {
this.tryFixSearchBlock(pendingNonStandardLineLimit)
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
@@ -623,7 +623,7 @@ class NewFileContentConstructor {
} else {
const appendToPendingNonStandardLines = canWritependingNonStandardLines
if (appendToPendingNonStandardLines) {
// 处理非标内容
// Handle non-standard content
this.pendingNonStandardLines.push(line)
}
}
@@ -734,7 +734,7 @@ class NewFileContentConstructor {
const replaceBeginTagRegexp = /^[=]{3,}$/
const replaceBeginTagIndex = this.findLastMatchingLineIndex(replaceBeginTagRegexp, lineLimit)
if (replaceBeginTagIndex !== -1) {
// // 校验非标内容
// // Validate non-standard content
// if (!this.isSearchingActive()) {
// removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
// }
@@ -765,7 +765,7 @@ class NewFileContentConstructor {
const replaceEndTagIndex = this.findLastMatchingLineIndex(replaceEndTagRegexp, lineLimit)
const likeReplaceEndTag = replaceEndTagIndex === lineLimit - 1
if (likeReplaceEndTag) {
// // 校验非标内容
// // Validate non-standard content
// if (!this.isReplacingActive()) {
// removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
// }
@@ -0,0 +1,69 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
import axios from "axios"
import { Controller } from ".."
/**
* Fetches available models from AIhubmix
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the AIhubmix models
*/
export async function getAihubmixModels(_controller: Controller, _request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
try {
const response = await axios.get("https://aihubmix.com/call/mdl_info_platform?tag=coding")
if (!response.data?.success || !Array.isArray(response.data?.data)) {
return OpenRouterCompatibleModelInfo.create({ models: {} })
}
// Raw data is an array and cannot be directly reused as a map; need to construct a separate modelsMap
const modelsArray = response.data.data as any[]
const modelsMap: Record<string, OpenRouterModelInfo> = {}
for (const modelData of modelsArray) {
if (!modelData.model || typeof modelData.model !== "string") {
continue
}
// Check if image support is available
const supportsImages =
modelData.modalities?.includes("vision") ||
modelData.modalities?.includes("image") ||
modelData.features?.includes("vision") ||
false
// Check if thinking/reasoning is supported
const supportsThinking = modelData.features?.includes("thinking") || false
// Check if caching is supported: cache_ratio is not 1 or cache read price differs from input price
const pricing = modelData.pricing || {}
const supportsPromptCache =
(modelData.cache_ratio !== undefined && modelData.cache_ratio !== 1) ||
(pricing.cache_read !== undefined && pricing.input !== undefined && pricing.cache_read !== pricing.input)
const modelId = modelData.model
modelsMap[modelId] = OpenRouterModelInfo.create({
maxTokens: modelData.max_output ?? 8192,
contextWindow: modelData.context_window ?? 128000,
supportsImages: supportsImages,
supportsPromptCache: supportsPromptCache,
inputPrice: pricing.input ?? 0,
outputPrice: pricing.output ?? 0,
cacheWritesPrice: pricing.cache_write ?? 0,
cacheReadsPrice: pricing.cache_read ?? 0,
description: modelData.desc_en || modelData.desc || "",
thinkingConfig: supportsThinking
? modelData.thinking_config
? modelData.thinking_config
: undefined
: undefined,
supportsGlobalEndpoint: modelData.supports_global_endpoint ?? undefined,
tiers: [],
})
}
return OpenRouterCompatibleModelInfo.create({ models: modelsMap })
} catch (error) {
return OpenRouterCompatibleModelInfo.create({ models: {} })
}
}
@@ -72,6 +72,9 @@ export async function updateApiConfigurationProto(
planModeOcaModelInfo: protoApiConfiguration.planModeOcaModelInfo
? fromProtobufOcaModelInfo(protoApiConfiguration.planModeOcaModelInfo)
: undefined,
planModeAihubmixModelInfo: protoApiConfiguration.planModeAihubmixModelInfo
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.planModeAihubmixModelInfo)
: undefined,
// Act Mode
actModeOpenRouterModelInfo: protoApiConfiguration.actModeOpenRouterModelInfo
@@ -104,6 +107,9 @@ export async function updateApiConfigurationProto(
actModeOcaModelInfo: protoApiConfiguration.actModeOcaModelInfo
? fromProtobufOcaModelInfo(protoApiConfiguration.actModeOcaModelInfo)
: undefined,
actModeAihubmixModelInfo: protoApiConfiguration.actModeAihubmixModelInfo
? fromProtobufOpenAiCompatibleModelInfo(protoApiConfiguration.actModeAihubmixModelInfo)
: undefined,
}
// Update the API configuration in storage
+21
View File
@@ -496,6 +496,9 @@ export class StateManager {
ocaMode,
hicapApiKey,
hicapModelId,
aihubmixApiKey,
aihubmixBaseUrl,
aihubmixAppCode,
// Plan mode configurations
planModeApiProvider,
planModeApiModelId,
@@ -532,6 +535,8 @@ export class StateManager {
planModeOcaModelInfo,
planModeHicapModelId,
planModeHicapModelInfo,
planModeAihubmixModelId,
planModeAihubmixModelInfo,
// Act mode configurations
actModeApiProvider,
actModeApiModelId,
@@ -568,6 +573,8 @@ export class StateManager {
actModeOcaModelInfo,
actModeHicapModelId,
actModeHicapModelInfo,
actModeAihubmixModelId,
actModeAihubmixModelInfo,
} = apiConfiguration
// Batch update global state keys
@@ -688,6 +695,8 @@ export class StateManager {
minimaxApiLine,
ocaMode,
hicapModelId,
aihubmixBaseUrl,
aihubmixAppCode,
})
// Batch update secrets
@@ -728,6 +737,7 @@ export class StateManager {
zaiApiKey,
minimaxApiKey,
hicapApiKey,
aihubmixApiKey,
})
}
@@ -1004,6 +1014,7 @@ export class StateManager {
zaiApiKey: this.secretsCache["zaiApiKey"],
minimaxApiKey: this.secretsCache["minimaxApiKey"],
hicapApiKey: this.secretsCache["hicapApiKey"],
aihubmixApiKey: this.secretsCache["aihubmixApiKey"],
// Global state (with remote config precedence for applicable fields)
awsRegion:
@@ -1076,6 +1087,8 @@ export class StateManager {
minimaxApiLine: this.taskStateCache["minimaxApiLine"] || this.globalStateCache["minimaxApiLine"],
ocaMode: this.globalStateCache["ocaMode"],
hicapModelId: this.globalStateCache["hicapModelId"],
aihubmixBaseUrl: this.taskStateCache["aihubmixBaseUrl"] || this.globalStateCache["aihubmixBaseUrl"],
aihubmixAppCode: this.taskStateCache["aihubmixAppCode"] || this.globalStateCache["aihubmixAppCode"],
// Plan mode configurations
planModeApiProvider:
@@ -1146,6 +1159,10 @@ export class StateManager {
planModeHicapModelId: this.taskStateCache["planModeHicapModelId"] || this.globalStateCache["planModeHicapModelId"],
planModeHicapModelInfo:
this.taskStateCache["planModeHicapModelInfo"] || this.globalStateCache["planModeHicapModelInfo"],
planModeAihubmixModelId:
this.taskStateCache["planModeAihubmixModelId"] || this.globalStateCache["planModeAihubmixModelId"],
planModeAihubmixModelInfo:
this.taskStateCache["planModeAihubmixModelInfo"] || this.globalStateCache["planModeAihubmixModelInfo"],
// Act mode configurations
actModeApiProvider:
@@ -1213,6 +1230,10 @@ export class StateManager {
actModeOcaModelInfo: this.globalStateCache["actModeOcaModelInfo"],
actModeHicapModelId: this.globalStateCache["actModeHicapModelId"],
actModeHicapModelInfo: this.globalStateCache["actModeHicapModelInfo"],
actModeAihubmixModelId:
this.taskStateCache["actModeAihubmixModelId"] || this.globalStateCache["actModeAihubmixModelId"],
actModeAihubmixModelInfo:
this.taskStateCache["actModeAihubmixModelInfo"] || this.globalStateCache["actModeAihubmixModelInfo"],
}
}
}
+20
View File
@@ -53,6 +53,7 @@ export async function readSecretsFromDisk(context: ExtensionContext): Promise<Se
ocaRefreshToken,
minimaxApiKey,
hicapApiKey,
aihubmixApiKey,
] = await Promise.all([
context.secrets.get("apiKey") as Promise<Secrets["apiKey"]>,
context.secrets.get("openRouterApiKey") as Promise<Secrets["openRouterApiKey"]>,
@@ -94,6 +95,7 @@ export async function readSecretsFromDisk(context: ExtensionContext): Promise<Se
context.secrets.get("ocaRefreshToken") as Promise<string | undefined>,
context.secrets.get("minimaxApiKey") as Promise<Secrets["minimaxApiKey"]>,
context.secrets.get("hicapApiKey") as Promise<Secrets["hicapApiKey"]>,
context.secrets.get("aihubmixApiKey") as Promise<Secrets["aihubmixApiKey"]>,
])
return {
@@ -137,6 +139,7 @@ export async function readSecretsFromDisk(context: ExtensionContext): Promise<Se
ocaRefreshToken,
minimaxApiKey,
hicapApiKey,
aihubmixApiKey,
}
}
@@ -267,6 +270,8 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
context.globalState.get<GlobalStateAndSettings["autoCondenseThreshold"]>("autoCondenseThreshold") // number from 0 to 1
const hooksEnabled = context.globalState.get<GlobalStateAndSettings["hooksEnabled"]>("hooksEnabled")
const hicapModelId = context.globalState.get<GlobalStateAndSettings["hicapModelId"]>("hicapModelId")
const aihubmixBaseUrl = context.globalState.get<GlobalStateAndSettings["aihubmixBaseUrl"]>("aihubmixBaseUrl")
const aihubmixAppCode = context.globalState.get<GlobalStateAndSettings["aihubmixAppCode"]>("aihubmixAppCode")
// OpenTelemetry configuration
const openTelemetryEnabled =
@@ -375,6 +380,10 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
context.globalState.get<GlobalStateAndSettings["planModeHicapModelId"]>("planModeHicapModelId")
const planModeHicapModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeHicapModelInfo"]>("planModeHicapModelInfo")
const planModeAihubmixModelId =
context.globalState.get<GlobalStateAndSettings["planModeAihubmixModelId"]>("planModeAihubmixModelId")
const planModeAihubmixModelInfo =
context.globalState.get<GlobalStateAndSettings["planModeAihubmixModelInfo"]>("planModeAihubmixModelInfo")
// Act mode configurations
const actModeApiProvider = context.globalState.get<GlobalStateAndSettings["actModeApiProvider"]>("actModeApiProvider")
const actModeApiModelId = context.globalState.get<GlobalStateAndSettings["actModeApiModelId"]>("actModeApiModelId")
@@ -446,6 +455,10 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
const actModeHicapModelId = context.globalState.get<GlobalStateAndSettings["actModeHicapModelId"]>("actModeHicapModelId")
const actModeHicapModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeHicapModelInfo"]>("actModeHicapModelInfo")
const actModeAihubmixModelId =
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelId"]>("actModeAihubmixModelId")
const actModeAihubmixModelInfo =
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelInfo"]>("actModeAihubmixModelInfo")
let apiProvider: ApiProvider
if (planModeApiProvider) {
@@ -526,6 +539,8 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
minimaxApiLine,
ocaMode: ocaMode || "internal",
hicapModelId,
aihubmixBaseUrl,
aihubmixAppCode,
// Plan mode configurations
planModeApiProvider: planModeApiProvider || apiProvider,
planModeApiModelId,
@@ -564,6 +579,8 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
planModeOcaModelInfo,
planModeHicapModelId,
planModeHicapModelInfo,
planModeAihubmixModelId,
planModeAihubmixModelInfo,
// Act mode configurations
actModeApiProvider: actModeApiProvider || apiProvider,
actModeApiModelId,
@@ -600,6 +617,8 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
actModeOcaModelInfo,
actModeHicapModelId,
actModeHicapModelInfo,
actModeAihubmixModelId,
actModeAihubmixModelInfo,
// Other global fields
focusChainSettings: focusChainSettings || DEFAULT_FOCUS_CHAIN_SETTINGS,
@@ -720,6 +739,7 @@ export async function resetGlobalState(controller: Controller) {
"ocaRefreshToken",
"minimaxApiKey",
"hicapApiKey",
"aihubmixApiKey",
]
await Promise.all(secretKeys.map((key) => context.secrets.delete(key)))
await controller.stateManager.reInitialize()
+10
View File
@@ -37,6 +37,7 @@ export type ApiProvider =
| "vercel-ai-gateway"
| "zai"
| "oca"
| "aihubmix"
| "minimax"
| "hicap"
@@ -46,6 +47,9 @@ export interface ApiHandlerSecrets {
awsAccessKey?: string
awsSecretKey?: string
openRouterApiKey?: string
aihubmixApiKey?: string
aihubmixBaseUrl?: string
aihubmixAppCode?: string
clineAccountId?: string
awsSessionToken?: string
@@ -128,6 +132,8 @@ export interface ApiHandlerOptions {
ocaBaseUrl?: string
minimaxApiLine?: string
ocaMode?: string
aihubmixBaseUrl?: string
aihubmixAppCode?: string
// Plan mode configurations
planModeApiModelId?: string
@@ -162,6 +168,8 @@ export interface ApiHandlerOptions {
planModeVercelAiGatewayModelInfo?: ModelInfo
planModeOcaModelId?: string
planModeOcaModelInfo?: OcaModelInfo
planModeAihubmixModelId?: string
planModeAihubmixModelInfo?: OpenAiCompatibleModelInfo
planModeHicapModelId?: string
planModeHicapModelInfo?: ModelInfo
// Act mode configurations
@@ -199,6 +207,8 @@ export interface ApiHandlerOptions {
actModeVercelAiGatewayModelInfo?: ModelInfo
actModeOcaModelId?: string
actModeOcaModelInfo?: OcaModelInfo
actModeAihubmixModelId?: string
actModeAihubmixModelInfo?: OpenAiCompatibleModelInfo
actModeHicapModelId?: string
actModeHicapModelInfo?: ModelInfo
}
@@ -307,6 +307,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.DIFY
case "oca":
return ProtoApiProvider.OCA
case "aihubmix":
return ProtoApiProvider.AIHUBMIX
case "minimax":
return ProtoApiProvider.MINIMAX
case "hicap":
@@ -393,6 +395,8 @@ export function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvid
return "dify"
case ProtoApiProvider.OCA:
return "oca"
case ProtoApiProvider.AIHUBMIX:
return "aihubmix"
case ProtoApiProvider.MINIMAX:
return "minimax"
default:
@@ -480,6 +484,9 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
minimaxApiKey: config.minimaxApiKey,
minimaxApiLine: config.minimaxApiLine,
ocaMode: config.ocaMode,
aihubmixApiKey: config.aihubmixApiKey,
aihubmixBaseUrl: config.aihubmixBaseUrl,
aihubmixAppCode: config.aihubmixAppCode,
hicapApiKey: config.hicapApiKey,
hicapModelId: config.hicapModelId,
@@ -517,6 +524,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
planModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.planModeVercelAiGatewayModelInfo),
planModeOcaModelId: config.planModeOcaModelId,
planModeOcaModelInfo: convertOcaModelInfoToProtoOcaModelInfo(config.planModeOcaModelInfo),
planModeAihubmixModelId: config.planModeAihubmixModelId,
planModeAihubmixModelInfo: convertOpenAiCompatibleModelInfoToProto(config.planModeAihubmixModelInfo),
planModeHicapModelId: config.planModeHicapModelId,
planModeHicapModelInfo: convertModelInfoToProtoOpenRouter(config.planModeHicapModelInfo),
@@ -554,6 +563,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
actModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.actModeVercelAiGatewayModelInfo),
actModeOcaModelId: config.actModeOcaModelId,
actModeOcaModelInfo: convertOcaModelInfoToProtoOcaModelInfo(config.actModeOcaModelInfo),
actModeAihubmixModelId: config.actModeAihubmixModelId,
actModeAihubmixModelInfo: convertOpenAiCompatibleModelInfoToProto(config.actModeAihubmixModelInfo),
actModeHicapModelId: config.actModeHicapModelId,
actModeHicapModelInfo: convertModelInfoToProtoOpenRouter(config.actModeHicapModelInfo),
}
@@ -637,6 +648,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
difyBaseUrl: protoConfig.difyBaseUrl,
ocaBaseUrl: protoConfig.ocaBaseUrl,
ocaMode: protoConfig.ocaMode,
aihubmixApiKey: protoConfig.aihubmixApiKey,
aihubmixBaseUrl: protoConfig.aihubmixBaseUrl,
aihubmixAppCode: protoConfig.aihubmixAppCode,
minimaxApiKey: protoConfig.minimaxApiKey,
minimaxApiLine: protoConfig.minimaxApiLine,
hicapApiKey: protoConfig.hicapApiKey,
@@ -679,6 +693,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
planModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.planModeVercelAiGatewayModelInfo),
planModeOcaModelId: protoConfig.planModeOcaModelId,
planModeOcaModelInfo: convertProtoOcaModelInfoToOcaModelInfo(protoConfig.planModeOcaModelInfo),
planModeAihubmixModelId: protoConfig.planModeAihubmixModelId,
planModeAihubmixModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.planModeAihubmixModelInfo),
planModeHicapModelId: protoConfig.planModeHicapModelId,
planModeHicapModelInfo: convertProtoToModelInfo(protoConfig.planModeHicapModelInfo),
@@ -717,6 +733,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
actModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.actModeVercelAiGatewayModelInfo),
actModeOcaModelId: protoConfig.actModeOcaModelId,
actModeOcaModelInfo: convertProtoOcaModelInfoToOcaModelInfo(protoConfig.actModeOcaModelInfo),
actModeAihubmixModelId: protoConfig.actModeAihubmixModelId,
actModeAihubmixModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.actModeAihubmixModelInfo),
actModeHicapModelId: protoConfig.actModeHicapModelId,
actModeHicapModelInfo: convertProtoToModelInfo(protoConfig.actModeHicapModelInfo),
}
+7
View File
@@ -109,6 +109,8 @@ export interface Settings {
ocaBaseUrl: string | undefined
minimaxApiLine: string | undefined
ocaMode: string | undefined
aihubmixBaseUrl: string | undefined
aihubmixAppCode: string | undefined
hooksEnabled: boolean
subagentsEnabled: boolean
hicapModelId: string | undefined
@@ -147,6 +149,8 @@ export interface Settings {
planModeOcaModelInfo: OcaModelInfo | undefined
planModeHicapModelId: string | undefined
planModeHicapModelInfo: ModelInfo | undefined
planModeAihubmixModelId: string | undefined
planModeAihubmixModelInfo: ModelInfo | undefined
// Act mode configurations
actModeApiProvider: ApiProvider
actModeApiModelId: string | undefined
@@ -185,6 +189,8 @@ export interface Settings {
actModeOcaModelInfo: OcaModelInfo | undefined
actModeHicapModelId: string | undefined
actModeHicapModelInfo: ModelInfo | undefined
actModeAihubmixModelId: string | undefined
actModeAihubmixModelInfo: ModelInfo | undefined
// OpenTelemetry configuration
openTelemetryEnabled: boolean
@@ -244,6 +250,7 @@ export interface Secrets {
ocaRefreshToken: string | undefined
minimaxApiKey: string | undefined
hicapApiKey: string | undefined
aihubmixApiKey: string | undefined
}
export interface LocalState {
@@ -12,6 +12,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { highlight } from "../history/HistoryView"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
import { AIhubmixProvider } from "./providers/AihubmixProvider"
import { AnthropicProvider } from "./providers/AnthropicProvider"
import { AskSageProvider } from "./providers/AskSageProvider"
import { BasetenProvider } from "./providers/BasetenProvider"
@@ -131,6 +132,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
let providers = [
{ value: "cline", label: "Cline" },
{ value: "openrouter", label: "OpenRouter" },
{ value: "aihubmix", label: "AIhubmix" },
{ value: "gemini", label: "Google Gemini" },
{ value: "openai", label: "OpenAI Compatible" },
{ value: "anthropic", label: "Anthropic" },
@@ -371,8 +373,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
key={item.value}
onClick={() => handleProviderChange(item.value)}
onMouseEnter={() => setSelectedIndex(index)}
ref={(el) => (itemRefs.current[index] = el)}>
<span dangerouslySetInnerHTML={{ __html: item.html }} />
ref={(el) => {
itemRefs.current[index] = el
}}>
<span>{item.html}</span>
</ProviderDropdownItem>
))}
</ProviderDropdownList>
@@ -526,6 +530,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{apiConfiguration && selectedProvider === "oca" && <OcaProvider currentMode={currentMode} isPopup={isPopup} />}
{apiConfiguration && selectedProvider === "aihubmix" && (
<AIhubmixProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
{apiErrorMessage && (
<p
style={{
@@ -0,0 +1,133 @@
import { ModelInfo } from "@shared/api"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Mode } from "@shared/storage/types"
import { useEffect, useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the AIhubmixProvider component
*/
interface AIhubmixProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
}
/**
* The AIhubmix provider configuration component
*/
export const AIhubmixProvider = ({ showModelOptions, isPopup, currentMode }: AIhubmixProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange, handleModeFieldsChange } = useApiConfigurationHandlers()
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const [models, setModels] = useState<Record<string, ModelInfo>>({})
// 保证当前选中的模型在下拉列表中可见
const ensureSelectedPresent = (base: Record<string, ModelInfo>): Record<string, ModelInfo> => {
if (selectedModelId && !base[selectedModelId]) {
const info = (selectedModelInfo as ModelInfo) || {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
}
return { ...base, [selectedModelId]: info }
}
return base
}
console.log("selectedModelId", selectedModelId)
console.log("selectedModelInfo", selectedModelInfo)
// Get the normalized configuration
// 先回显本地缓存数据,再异步刷新并持久化到 localStorage
useEffect(() => {
try {
const cached = window.localStorage.getItem("aihubmixModels")
if (cached) {
const parsed = JSON.parse(cached) as Record<string, ModelInfo>
if (parsed && typeof parsed === "object") {
setModels(ensureSelectedPresent(parsed))
}
}
} catch {
// 解析失败则回显空集,仅注入当前选中模型(若有)
setModels(ensureSelectedPresent({}))
}
// 异步刷新模型列表
ModelsServiceClient.getAihubmixModels(EmptyRequest.create({}))
.then((response) => {
if (response.models) {
const nextModels = response.models as Record<string, ModelInfo>
const injected = ensureSelectedPresent(nextModels)
setModels(injected)
try {
window.localStorage.setItem("aihubmixModels", JSON.stringify(injected))
} catch {}
}
})
.catch((error) => {
console.error("Failed to fetch AIhubmix models:", error)
// 失败时保持当前 models,不打断用户
})
}, [])
console.log("apiConfiguration", apiConfiguration)
return (
<div>
<ApiKeyField
helpText="Now request 10% discount!"
initialValue={apiConfiguration?.aihubmixApiKey || ""}
onChange={(value) => handleFieldChange("aihubmixApiKey", value)}
providerName="AIhubmix"
signupUrl="https://console.aihubmix.com/token" // 转英文
/>
{showModelOptions && (
<>
<ModelSelector
label="Model"
models={models}
onChange={(e) => {
const newModelId = e.target.value
const newModelInfo = models[newModelId] as ModelInfo | undefined
// 同步保存 ID 和 ModelInfo,避免切换后丢失
if (newModelInfo) {
handleModeFieldsChange(
{
id: { plan: "planModeAihubmixModelId", act: "actModeAihubmixModelId" },
info: { plan: "planModeAihubmixModelInfo", act: "actModeAihubmixModelInfo" },
},
{ id: newModelId, info: newModelInfo },
currentMode,
)
// 不同步写全局字段,保持 AIhubmix 与全局字段隔离
} else {
// 仅保存 ID(无信息时退化)
handleModeFieldChange(
{ plan: "planModeAihubmixModelId", act: "actModeAihubmixModelId" },
newModelId,
currentMode,
)
// 不同步写全局字段
}
}}
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
</>
)}
</div>
)
}
@@ -86,6 +86,7 @@ export function normalizeApiConfiguration(
): NormalizedApiConfig {
const provider =
(currentMode === "plan" ? apiConfiguration?.planModeApiProvider : apiConfiguration?.actModeApiProvider) || "anthropic"
const modelId = currentMode === "plan" ? apiConfiguration?.planModeApiModelId : apiConfiguration?.actModeApiModelId
const getProviderData = (models: Record<string, ModelInfo>, defaultId: string) => {
@@ -368,6 +369,16 @@ export function normalizeApiConfiguration(
selectedModelId: ocaModelId || "",
selectedModelInfo: ocaModelInfo || liteLlmModelInfoSaneDefaults,
}
case "aihubmix":
const aihubmixModelId =
currentMode === "plan" ? apiConfiguration?.planModeAihubmixModelId : apiConfiguration?.actModeAihubmixModelId
const aihubmixModelInfo =
currentMode === "plan" ? apiConfiguration?.planModeAihubmixModelInfo : apiConfiguration?.actModeAihubmixModelInfo
return {
selectedProvider: provider,
selectedModelId: aihubmixModelId || "",
selectedModelInfo: aihubmixModelInfo || openAiModelInfoSaneDefaults,
}
case "minimax":
return getProviderData(minimaxModels, minimaxDefaultModelId)
default:
@@ -403,6 +414,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
huaweiCloudMaasModelId: undefined,
vercelAiGatewayModelId: undefined,
hicapModelId: undefined,
aihubmixModelId: undefined,
// Model info objects
openAiModelInfo: undefined,
@@ -414,6 +426,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
huggingFaceModelInfo: undefined,
vercelAiGatewayModelInfo: undefined,
vsCodeLmModelSelector: undefined,
aihubmixModelInfo: undefined,
// AWS Bedrock fields
awsBedrockCustomSelected: undefined,
@@ -453,6 +466,7 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
mode === "plan" ? apiConfiguration.planModeVercelAiGatewayModelId : apiConfiguration.actModeVercelAiGatewayModelId,
ocaModelId: mode === "plan" ? apiConfiguration.planModeOcaModelId : apiConfiguration.actModeOcaModelId,
hicapModelId: mode === "plan" ? apiConfiguration.planModeHicapModelId : apiConfiguration.actModeHicapModelId,
aihubmixModelId: mode === "plan" ? apiConfiguration.planModeAihubmixModelId : apiConfiguration.actModeAihubmixModelId,
// Model info objects
openAiModelInfo: mode === "plan" ? apiConfiguration.planModeOpenAiModelInfo : apiConfiguration.actModeOpenAiModelInfo,
@@ -472,6 +486,8 @@ export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undef
vsCodeLmModelSelector:
mode === "plan" ? apiConfiguration.planModeVsCodeLmModelSelector : apiConfiguration.actModeVsCodeLmModelSelector,
hicapModelInfo: mode === "plan" ? apiConfiguration.planModeHicapModelInfo : apiConfiguration.actModeHicapModelInfo,
aihubmixModelInfo:
mode === "plan" ? apiConfiguration.planModeAihubmixModelInfo : apiConfiguration.actModeAihubmixModelInfo,
// AWS Bedrock fields
awsBedrockCustomSelected:
@@ -646,6 +662,13 @@ export async function syncModeConfigurations(
updates.actModeOcaModelInfo = sourceFields.ocaModelInfo
break
case "aihubmix":
updates.planModeAihubmixModelId = sourceFields.aihubmixModelId
updates.planModeAihubmixModelInfo = sourceFields.aihubmixModelInfo
updates.actModeAihubmixModelId = sourceFields.aihubmixModelId
updates.actModeAihubmixModelInfo = sourceFields.aihubmixModelInfo
break
// Providers that use apiProvider + apiModelId fields
case "anthropic":
case "claude-code":