mirror of
https://github.com/cline/cline.git
synced 2026-09-05 14:14:01 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb9cdd33b2 | |||
| c74dcbbe2f | |||
| e69a5284fa | |||
| c472eea632 | |||
| 40be38c8d4 | |||
| 3908daafa9 | |||
| aa4f6d3192 | |||
| 8a25abae7a | |||
| d8999415e4 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add new provider AIhubmix
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user