mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat(xai): add dynamic model discovery with correct pricing
- Implemented dynamic model discovery for xAI provider using /v1/language-models endpoint - Models are now fetched at runtime with real-time pricing and capabilities - Fixed pricing conversion: XAI API returns fractional cents (basis points), divide by 10,000 not 100 - This fixes pricing display showing $20.00 instead of $0.20 per 1M tokens - Removed static xAI models from MODELS_BY_PROVIDER to rely on dynamic discovery - Enhanced error logging with detailed status and URL information - Support dynamic model context window overrides from API - Fixed parseApiPrice to handle zero values correctly (for free models) - Provided complete ModelInfo fallback in useSelectedModel for UI type safety - Added comprehensive test coverage for cost utilities and XAI fetcher - Updated all tests to reflect correct pricing scale and dynamic model behavior
This commit is contained in:
parent
8e4b145681
commit
a981925065
19 changed files with 619 additions and 91 deletions
|
|
@ -21,7 +21,6 @@ import {
|
|||
sambaNovaModels,
|
||||
vertexModels,
|
||||
vscodeLlmModels,
|
||||
xaiModels,
|
||||
internationalZAiModels,
|
||||
minimaxModels,
|
||||
} from "./providers/index.js"
|
||||
|
|
@ -50,6 +49,7 @@ export const dynamicProviders = [
|
|||
"glama",
|
||||
"roo",
|
||||
"chutes",
|
||||
"xai",
|
||||
] as const
|
||||
|
||||
export type DynamicProvider = (typeof dynamicProviders)[number]
|
||||
|
|
@ -137,7 +137,6 @@ export const providerNames = [
|
|||
"roo",
|
||||
"sambanova",
|
||||
"vertex",
|
||||
"xai",
|
||||
"zai",
|
||||
] as const
|
||||
|
||||
|
|
@ -354,6 +353,7 @@ const fakeAiSchema = baseProviderSettingsSchema.extend({
|
|||
|
||||
const xaiSchema = apiModelIdProviderModelSchema.extend({
|
||||
xaiApiKey: z.string().optional(),
|
||||
xaiModelContextWindow: z.number().optional(),
|
||||
})
|
||||
|
||||
const groqSchema = apiModelIdProviderModelSchema.extend({
|
||||
|
|
@ -709,7 +709,7 @@ export const MODELS_BY_PROVIDER: Record<
|
|||
label: "VS Code LM API",
|
||||
models: Object.keys(vscodeLlmModels),
|
||||
},
|
||||
xai: { id: "xai", label: "xAI (Grok)", models: Object.keys(xaiModels) },
|
||||
xai: { id: "xai", label: "xAI (Grok)", models: [] },
|
||||
zai: { id: "zai", label: "Zai", models: Object.keys(internationalZAiModels) },
|
||||
|
||||
// Dynamic providers; models pulled from remote APIs.
|
||||
|
|
|
|||
|
|
@ -3,93 +3,63 @@ import type { ModelInfo } from "../model.js"
|
|||
// https://docs.x.ai/docs/api-reference
|
||||
export type XAIModelId = keyof typeof xaiModels
|
||||
|
||||
export const xaiDefaultModelId: XAIModelId = "grok-code-fast-1"
|
||||
export const xaiDefaultModelId: XAIModelId = "grok-4-fast-reasoning"
|
||||
|
||||
/**
|
||||
* Partial ModelInfo for xAI static registry.
|
||||
* Contains only fields not available from the xAI API:
|
||||
* - contextWindow: Not provided by API
|
||||
* - maxTokens: Not provided by API
|
||||
* - description: User-friendly descriptions
|
||||
* - supportsReasoningEffort: Special capability flag
|
||||
*
|
||||
* All other fields (pricing, supportsPromptCache, supportsImages) are fetched dynamically.
|
||||
*/
|
||||
type XAIStaticModelInfo = Pick<ModelInfo, "contextWindow" | "description"> & {
|
||||
maxTokens?: number | null
|
||||
supportsReasoningEffort?: boolean
|
||||
}
|
||||
|
||||
export const xaiModels = {
|
||||
"grok-code-fast-1": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 262_144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 1.5,
|
||||
cacheWritesPrice: 0.02,
|
||||
cacheReadsPrice: 0.02,
|
||||
contextWindow: 256_000,
|
||||
description: "xAI's Grok Code Fast model with 256K context window",
|
||||
},
|
||||
"grok-4": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 256000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 0.75,
|
||||
cacheReadsPrice: 0.75,
|
||||
"grok-4-0709": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 256_000,
|
||||
description: "xAI's Grok-4 model with 256K context window",
|
||||
},
|
||||
"grok-4-fast-non-reasoning": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 2_000_000,
|
||||
description: "xAI's Grok-4 Fast Non-Reasoning model with 2M context window",
|
||||
},
|
||||
"grok-4-fast-reasoning": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 2_000_000,
|
||||
description: "xAI's Grok-4 Fast Reasoning model with 2M context window",
|
||||
},
|
||||
"grok-3": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 0.75,
|
||||
cacheReadsPrice: 0.75,
|
||||
contextWindow: 131_072,
|
||||
description: "xAI's Grok-3 model with 128K context window",
|
||||
},
|
||||
"grok-3-fast": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 25.0,
|
||||
cacheWritesPrice: 1.25,
|
||||
cacheReadsPrice: 1.25,
|
||||
description: "xAI's Grok-3 fast model with 128K context window",
|
||||
},
|
||||
"grok-3-mini": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.5,
|
||||
cacheWritesPrice: 0.07,
|
||||
cacheReadsPrice: 0.07,
|
||||
contextWindow: 131_072,
|
||||
description: "xAI's Grok-3 mini model with 128K context window",
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"grok-3-mini-fast": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 4.0,
|
||||
cacheWritesPrice: 0.15,
|
||||
cacheReadsPrice: 0.15,
|
||||
description: "xAI's Grok-3 mini fast model with 128K context window",
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"grok-2-1212": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
description: "xAI's Grok-2 model (version 1212) with 128K context window",
|
||||
contextWindow: 32_768,
|
||||
description: "xAI's Grok-2 model (version 1212) with 32K context window",
|
||||
},
|
||||
"grok-2-vision-1212": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 32768,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
contextWindow: 32_768,
|
||||
description: "xAI's Grok-2 Vision model (version 1212) with image support and 32K context window",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
} as const satisfies Record<string, XAIStaticModelInfo>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,12 @@ describe("XAIHandler", () => {
|
|||
it("should return default model when no model is specified", () => {
|
||||
const model = handler.getModel()
|
||||
expect(model.id).toBe(xaiDefaultModelId)
|
||||
expect(model.info).toEqual(xaiModels[xaiDefaultModelId])
|
||||
expect(model.info).toMatchObject({
|
||||
contextWindow: xaiModels[xaiDefaultModelId].contextWindow,
|
||||
maxTokens: xaiModels[xaiDefaultModelId].maxTokens,
|
||||
description: xaiModels[xaiDefaultModelId].description,
|
||||
})
|
||||
expect(model.info.supportsPromptCache).toBe(false) // Placeholder until dynamic data loads
|
||||
})
|
||||
|
||||
test("should return specified model when valid model is provided", () => {
|
||||
|
|
@ -66,7 +71,12 @@ describe("XAIHandler", () => {
|
|||
const model = handlerWithModel.getModel()
|
||||
|
||||
expect(model.id).toBe(testModelId)
|
||||
expect(model.info).toEqual(xaiModels[testModelId])
|
||||
expect(model.info).toMatchObject({
|
||||
contextWindow: xaiModels[testModelId].contextWindow,
|
||||
maxTokens: xaiModels[testModelId].maxTokens,
|
||||
description: xaiModels[testModelId].description,
|
||||
})
|
||||
expect(model.info.supportsPromptCache).toBe(false) // Placeholder until dynamic data loads
|
||||
})
|
||||
|
||||
it("should include reasoning_effort parameter for mini models", async () => {
|
||||
|
|
@ -234,12 +244,13 @@ describe("XAIHandler", () => {
|
|||
|
||||
// Verify the usage data
|
||||
expect(firstChunk.done).toBe(false)
|
||||
expect(firstChunk.value).toEqual({
|
||||
expect(firstChunk.value).toMatchObject({
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
cacheReadTokens: 5,
|
||||
cacheWriteTokens: 15,
|
||||
totalCost: expect.any(Number),
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
56
src/api/providers/fetchers/__tests__/xai.spec.ts
Normal file
56
src/api/providers/fetchers/__tests__/xai.spec.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import axios from "axios"
|
||||
|
||||
vi.mock("axios")
|
||||
|
||||
import { getXaiModels } from "../xai"
|
||||
import { xaiModels } from "@roo-code/types"
|
||||
|
||||
describe("getXaiModels", () => {
|
||||
const mockedAxios = axios as unknown as { get: ReturnType<typeof vi.fn> }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns mapped models with pricing and modalities (augmenting static info when available)", async () => {
|
||||
mockedAxios.get = vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
models: [
|
||||
{
|
||||
id: "grok-3",
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
prompt_text_token_price: 2000, // 2000 fractional cents = $0.20 per 1M tokens
|
||||
cached_prompt_text_token_price: 500, // 500 fractional cents = $0.05 per 1M tokens
|
||||
completion_text_token_price: 10000, // 10000 fractional cents = $1.00 per 1M tokens
|
||||
aliases: ["grok-3-latest"],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const result = await getXaiModels("key", "https://api.x.ai/v1")
|
||||
expect(result["grok-3"]).toBeDefined()
|
||||
expect(result["grok-3"]?.supportsImages).toBe(false)
|
||||
expect(result["grok-3"]?.inputPrice).toBeCloseTo(0.2) // $0.20 per 1M tokens
|
||||
expect(result["grok-3"]?.outputPrice).toBeCloseTo(1.0) // $1.00 per 1M tokens
|
||||
expect(result["grok-3"]?.cacheReadsPrice).toBeCloseTo(0.05) // $0.05 per 1M tokens
|
||||
// aliases are not added to avoid UI duplication
|
||||
expect(result["grok-3-latest"]).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns empty object on schema mismatches (graceful degradation)", async () => {
|
||||
mockedAxios.get = vi.fn().mockResolvedValue({
|
||||
data: { data: [{ bogus: true }] },
|
||||
})
|
||||
const result = await getXaiModels("key")
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it("includes Authorization header when apiKey provided", async () => {
|
||||
mockedAxios.get = vi.fn().mockResolvedValue({ data: { data: [] } })
|
||||
await getXaiModels("secret")
|
||||
expect((axios.get as any).mock.calls[0][1].headers.Authorization).toBe("Bearer secret")
|
||||
})
|
||||
})
|
||||
|
|
@ -26,6 +26,7 @@ import { getDeepInfraModels } from "./deepinfra"
|
|||
import { getHuggingFaceModels } from "./huggingface"
|
||||
import { getRooModels } from "./roo"
|
||||
import { getChutesModels } from "./chutes"
|
||||
import { getXaiModels } from "./xai"
|
||||
|
||||
const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
|
||||
|
||||
|
|
@ -101,6 +102,9 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
|
|||
case "huggingface":
|
||||
models = await getHuggingFaceModels()
|
||||
break
|
||||
case "xai":
|
||||
models = await getXaiModels(options.apiKey, options.baseUrl)
|
||||
break
|
||||
case "roo": {
|
||||
// Roo Code Cloud provider requires baseUrl and optional apiKey
|
||||
const rooBaseUrl =
|
||||
|
|
@ -121,7 +125,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
|
|||
// Cache the fetched models (even if empty, to signify a successful fetch with no models).
|
||||
memoryCache.set(provider, models)
|
||||
|
||||
await writeModels(provider, models).catch((err) =>
|
||||
await writeModels(provider, models || {}).catch((err) =>
|
||||
console.error(`[getModels] Error writing ${provider} models to file cache:`, err),
|
||||
)
|
||||
|
||||
|
|
|
|||
107
src/api/providers/fetchers/xai.ts
Normal file
107
src/api/providers/fetchers/xai.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import axios from "axios"
|
||||
import { z } from "zod"
|
||||
|
||||
import { type ModelInfo, xaiModels } from "@roo-code/types"
|
||||
import { DEFAULT_HEADERS } from "../../providers/constants"
|
||||
|
||||
/**
|
||||
* Schema for GET https://api.x.ai/v1/language-models
|
||||
* This endpoint returns rich metadata including modalities and pricing.
|
||||
*/
|
||||
const xaiLanguageModelSchema = z.object({
|
||||
id: z.string(),
|
||||
input_modalities: z.array(z.string()).optional(),
|
||||
output_modalities: z.array(z.string()).optional(),
|
||||
prompt_text_token_price: z.number().optional(), // fractional cents (basis points) per 1M tokens
|
||||
cached_prompt_text_token_price: z.number().optional(), // fractional cents per 1M tokens
|
||||
prompt_image_token_price: z.number().optional(), // fractional cents per 1M tokens
|
||||
completion_text_token_price: z.number().optional(), // fractional cents per 1M tokens
|
||||
search_price: z.number().optional(),
|
||||
aliases: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
const xaiLanguageModelsResponseSchema = z.object({
|
||||
models: z.array(xaiLanguageModelSchema),
|
||||
})
|
||||
|
||||
/**
|
||||
* Fetch available xAI models for the authenticated account.
|
||||
* - Uses Bearer Authorization header when apiKey is provided
|
||||
* - Maps discovered IDs to ModelInfo using static catalog (xaiModels) when possible
|
||||
* - For models not in static catalog, contextWindow and maxTokens remain undefined
|
||||
*/
|
||||
export async function getXaiModels(apiKey?: string, baseUrl?: string): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
// Build proper endpoint whether user passes https://api.x.ai or https://api.x.ai/v1
|
||||
const base = baseUrl ? baseUrl.replace(/\/+$/, "") : "https://api.x.ai"
|
||||
const url = base.endsWith("/v1") ? `${base}/language-models` : `${base}/v1/language-models`
|
||||
|
||||
try {
|
||||
const resp = await axios.get(url, {
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
Accept: "application/json",
|
||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
const parsed = xaiLanguageModelsResponseSchema.safeParse(resp.data)
|
||||
const items = parsed.success
|
||||
? parsed.data.models
|
||||
: Array.isArray((resp.data as any)?.models)
|
||||
? (resp.data as any)?.models
|
||||
: []
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("xAI language models response validation failed", parsed.error?.format?.() ?? parsed.error)
|
||||
}
|
||||
|
||||
// Helper to convert fractional-cents-per-1M (basis points) to dollars-per-1M
|
||||
// The API returns values in 1/100th of a cent, so divide by 10,000 to get dollars
|
||||
const centsToDollars = (v?: number) => (typeof v === "number" ? v / 10_000 : undefined)
|
||||
|
||||
for (const m of items) {
|
||||
const id = m.id
|
||||
const staticInfo = xaiModels[id as keyof typeof xaiModels]
|
||||
const supportsImages = Array.isArray(m.input_modalities) ? m.input_modalities.includes("image") : false
|
||||
|
||||
// Cache support is indicated by presence of cached_prompt_text_token_price field (even if 0)
|
||||
const supportsPromptCache = typeof m.cached_prompt_text_token_price === "number"
|
||||
const cacheReadsPrice = supportsPromptCache ? centsToDollars(m.cached_prompt_text_token_price) : undefined
|
||||
|
||||
const info: ModelInfo = {
|
||||
maxTokens: staticInfo?.maxTokens ?? undefined,
|
||||
contextWindow: staticInfo?.contextWindow ?? undefined,
|
||||
supportsImages,
|
||||
supportsPromptCache,
|
||||
inputPrice: centsToDollars(m.prompt_text_token_price),
|
||||
outputPrice: centsToDollars(m.completion_text_token_price),
|
||||
cacheReadsPrice,
|
||||
cacheWritesPrice: cacheReadsPrice, // xAI uses same price for reads and writes
|
||||
description: staticInfo?.description,
|
||||
supportsReasoningEffort:
|
||||
staticInfo && "supportsReasoningEffort" in staticInfo
|
||||
? staticInfo.supportsReasoningEffort
|
||||
: undefined,
|
||||
// leave other optional fields undefined unless available via static definitions
|
||||
}
|
||||
|
||||
models[id] = info
|
||||
// Aliases are not added to the model list to avoid duplication in UI
|
||||
// Users should use the primary model ID; xAI API will handle alias resolution
|
||||
}
|
||||
} catch (error) {
|
||||
// Avoid logging sensitive data like Authorization headers
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status
|
||||
const statusText = error.response?.statusText
|
||||
const url = (error as any)?.config?.url
|
||||
console.error(`[xAI] models fetch failed: ${status ?? "unknown"} ${statusText ?? ""} ${url ?? ""}`.trim())
|
||||
} else {
|
||||
console.error("[xAI] models fetch failed.", error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
import { type XAIModelId, xaiDefaultModelId, xaiModels } from "@roo-code/types"
|
||||
import { type XAIModelId, xaiDefaultModelId, xaiModels, type ModelInfo } from "@roo-code/types"
|
||||
|
||||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
|
||||
|
|
@ -13,6 +13,9 @@ import { DEFAULT_HEADERS } from "./constants"
|
|||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { handleOpenAIError } from "./utils/openai-error-handler"
|
||||
import { calculateApiCostOpenAI } from "../../shared/cost"
|
||||
import type { ModelRecord } from "../../shared/api"
|
||||
import { getModels } from "./fetchers/modelCache"
|
||||
|
||||
const XAI_DEFAULT_TEMPERATURE = 0
|
||||
|
||||
|
|
@ -20,6 +23,7 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
|
|||
protected options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private readonly providerName = "xAI"
|
||||
protected models: ModelRecord = {}
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
|
@ -35,21 +39,49 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
|
|||
}
|
||||
|
||||
override getModel() {
|
||||
const id =
|
||||
this.options.apiModelId && this.options.apiModelId in xaiModels
|
||||
? (this.options.apiModelId as XAIModelId)
|
||||
: xaiDefaultModelId
|
||||
// Allow any model ID (dynamic discovery) and augment with static info when available
|
||||
const id = this.options.apiModelId ?? xaiDefaultModelId
|
||||
|
||||
const staticInfo = (xaiModels as Record<string, any>)[id as any]
|
||||
const dynamicInfo = this.models?.[id as any]
|
||||
|
||||
// Build complete ModelInfo using dynamic pricing/capabilities when available
|
||||
const info: ModelInfo = {
|
||||
contextWindow: this.options.xaiModelContextWindow ?? staticInfo?.contextWindow,
|
||||
maxTokens: staticInfo?.maxTokens ?? undefined,
|
||||
supportsPromptCache: dynamicInfo?.supportsPromptCache ?? false,
|
||||
supportsImages: dynamicInfo?.supportsImages,
|
||||
inputPrice: dynamicInfo?.inputPrice,
|
||||
outputPrice: dynamicInfo?.outputPrice,
|
||||
cacheReadsPrice: dynamicInfo?.cacheReadsPrice,
|
||||
cacheWritesPrice: dynamicInfo?.cacheWritesPrice,
|
||||
description: staticInfo?.description,
|
||||
supportsReasoningEffort:
|
||||
staticInfo && "supportsReasoningEffort" in staticInfo ? staticInfo.supportsReasoningEffort : undefined,
|
||||
}
|
||||
|
||||
const info = xaiModels[id]
|
||||
const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options })
|
||||
return { id, info, ...params }
|
||||
}
|
||||
|
||||
private async loadDynamicModels(): Promise<void> {
|
||||
try {
|
||||
this.models = await getModels({
|
||||
provider: "xai",
|
||||
apiKey: this.options.xaiApiKey,
|
||||
baseUrl: (this.client as any).baseURL || "https://api.x.ai/v1",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[XAI] Error loading dynamic models:", error)
|
||||
}
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
await this.loadDynamicModels()
|
||||
const { id: modelId, info: modelInfo, reasoning } = this.getModel()
|
||||
|
||||
// Use the OpenAI-compatible API.
|
||||
|
|
@ -98,12 +130,21 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
|
|||
const writeTokens =
|
||||
"cache_creation_input_tokens" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0
|
||||
|
||||
const totalCost = calculateApiCostOpenAI(
|
||||
modelInfo,
|
||||
chunk.usage.prompt_tokens || 0,
|
||||
chunk.usage.completion_tokens || 0,
|
||||
writeTokens || 0,
|
||||
readTokens || 0,
|
||||
)
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
cacheReadTokens: readTokens,
|
||||
cacheWriteTokens: writeTokens,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2720,6 +2720,7 @@ describe("ClineProvider - Router Models", () => {
|
|||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
xai: {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
|
|
@ -2776,6 +2777,7 @@ describe("ClineProvider - Router Models", () => {
|
|||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
xai: {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
|
|
@ -2900,6 +2902,7 @@ describe("ClineProvider - Router Models", () => {
|
|||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
xai: {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
xai: {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
|
|
@ -349,6 +350,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
xai: {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
|
|
@ -380,7 +382,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
type: "requestRouterModels",
|
||||
})
|
||||
|
||||
// Verify error messages were sent for failed providers (these come first)
|
||||
// Verify error messages were sent for failed providers
|
||||
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "singleRouterModelFetchResponse",
|
||||
success: false,
|
||||
|
|
@ -426,6 +428,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
"vercel-ai-gateway": mockModels,
|
||||
huggingface: {},
|
||||
"io-intelligence": {},
|
||||
xai: {},
|
||||
},
|
||||
values: undefined,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -777,6 +777,7 @@ export const webviewMessageHandler = async (
|
|||
lmstudio: {},
|
||||
roo: {},
|
||||
chutes: {},
|
||||
xai: {},
|
||||
}
|
||||
|
||||
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
|
||||
|
|
@ -838,6 +839,14 @@ export const webviewMessageHandler = async (
|
|||
})
|
||||
}
|
||||
|
||||
// Add xAI if API key is provided.
|
||||
if (apiConfiguration.xaiApiKey) {
|
||||
candidates.push({
|
||||
key: "xai",
|
||||
options: { provider: "xai", apiKey: apiConfiguration.xaiApiKey, baseUrl: "https://api.x.ai/v1" },
|
||||
})
|
||||
}
|
||||
|
||||
// LiteLLM is conditional on baseUrl+apiKey
|
||||
const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
|
||||
const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
|
||||
|
|
|
|||
114
src/shared/__tests__/cost.spec.ts
Normal file
114
src/shared/__tests__/cost.spec.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { describe, expect, it } from "vitest"
|
||||
import { parseApiPrice, calculateApiCostAnthropic, calculateApiCostOpenAI } from "../cost"
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
|
||||
describe("parseApiPrice", () => {
|
||||
it("should handle zero as a number", () => {
|
||||
expect(parseApiPrice(0)).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle zero as a string", () => {
|
||||
expect(parseApiPrice("0")).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle positive numbers", () => {
|
||||
expect(parseApiPrice(0.0002)).toBe(200)
|
||||
expect(parseApiPrice(0.00002)).toBe(20)
|
||||
})
|
||||
|
||||
it("should handle positive number strings", () => {
|
||||
expect(parseApiPrice("0.0002")).toBe(200)
|
||||
expect(parseApiPrice("0.00002")).toBe(20)
|
||||
})
|
||||
|
||||
it("should return undefined for null", () => {
|
||||
expect(parseApiPrice(null)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return undefined for undefined", () => {
|
||||
expect(parseApiPrice(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return undefined for empty string", () => {
|
||||
expect(parseApiPrice("")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateApiCostAnthropic", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 300,
|
||||
outputPrice: 1500,
|
||||
cacheWritesPrice: 375,
|
||||
cacheReadsPrice: 30,
|
||||
}
|
||||
|
||||
it("should calculate cost without caching", () => {
|
||||
const cost = calculateApiCostAnthropic(modelInfo, 1000, 500)
|
||||
expect(cost).toBeCloseTo(0.3 + 0.75, 10)
|
||||
})
|
||||
|
||||
it("should calculate cost with cache creation", () => {
|
||||
const cost = calculateApiCostAnthropic(modelInfo, 1000, 500, 2000)
|
||||
expect(cost).toBeCloseTo(0.3 + 0.75 + 0.75, 10)
|
||||
})
|
||||
|
||||
it("should calculate cost with cache reads", () => {
|
||||
const cost = calculateApiCostAnthropic(modelInfo, 1000, 500, 0, 3000)
|
||||
expect(cost).toBeCloseTo(0.3 + 0.75 + 0.09, 10)
|
||||
})
|
||||
|
||||
it("should handle zero cost for free models", () => {
|
||||
const freeModel: ModelInfo = {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
}
|
||||
const cost = calculateApiCostAnthropic(freeModel, 1000, 500)
|
||||
expect(cost).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateApiCostOpenAI", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 128000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 150,
|
||||
outputPrice: 600,
|
||||
cacheWritesPrice: 187.5,
|
||||
cacheReadsPrice: 15,
|
||||
}
|
||||
|
||||
it("should calculate cost without caching", () => {
|
||||
const cost = calculateApiCostOpenAI(modelInfo, 1000, 500)
|
||||
expect(cost).toBeCloseTo(0.15 + 0.3, 10)
|
||||
})
|
||||
|
||||
it("should subtract cached tokens from input tokens", () => {
|
||||
const cost = calculateApiCostOpenAI(modelInfo, 5000, 500, 2000, 1000)
|
||||
// 5000 total - 2000 cache creation - 1000 cache read = 2000 non-cached
|
||||
// Cost: (2000 * 0.00015) + (2000 * 0.0001875) + (1000 * 0.000015) + (500 * 0.0006)
|
||||
expect(cost).toBeCloseTo(0.3 + 0.375 + 0.015 + 0.3, 10)
|
||||
})
|
||||
|
||||
it("should handle zero cost for free models", () => {
|
||||
const freeModel: ModelInfo = {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
}
|
||||
const cost = calculateApiCostOpenAI(freeModel, 1000, 500)
|
||||
expect(cost).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -165,6 +165,7 @@ const dynamicProviderExtras = {
|
|||
lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
|
||||
roo: {} as { apiKey?: string; baseUrl?: string },
|
||||
chutes: {} as { apiKey?: string },
|
||||
xai: {} as { apiKey?: string; baseUrl?: string },
|
||||
} as const satisfies Record<RouterName, object>
|
||||
|
||||
// Build the dynamic options union from the map, intersected with CommonFetchParams
|
||||
|
|
|
|||
|
|
@ -80,4 +80,8 @@ export function calculateApiCostOpenAI(
|
|||
)
|
||||
}
|
||||
|
||||
export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined)
|
||||
export const parseApiPrice = (price: any) => {
|
||||
if (price == null) return undefined
|
||||
const parsed = parseFloat(price)
|
||||
return isNaN(parsed) ? undefined : parsed * 1_000_000
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,7 +235,8 @@ const ApiOptions = ({
|
|||
} else if (
|
||||
selectedProvider === "litellm" ||
|
||||
selectedProvider === "deepinfra" ||
|
||||
selectedProvider === "roo"
|
||||
selectedProvider === "roo" ||
|
||||
selectedProvider === "xai"
|
||||
) {
|
||||
vscode.postMessage({ type: "requestRouterModels" })
|
||||
}
|
||||
|
|
@ -252,6 +253,7 @@ const ApiOptions = ({
|
|||
apiConfiguration?.litellmApiKey,
|
||||
apiConfiguration?.deepInfraApiKey,
|
||||
apiConfiguration?.deepInfraBaseUrl,
|
||||
apiConfiguration?.xaiApiKey,
|
||||
customHeaders,
|
||||
],
|
||||
)
|
||||
|
|
@ -609,7 +611,14 @@ const ApiOptions = ({
|
|||
)}
|
||||
|
||||
{selectedProvider === "xai" && (
|
||||
<XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
|
||||
<XAI
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
routerModels={routerModels}
|
||||
refetchRouterModels={refetchRouterModels}
|
||||
organizationAllowList={organizationAllowList}
|
||||
modelValidationError={modelValidationError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "groq" && (
|
||||
|
|
@ -700,7 +709,7 @@ const ApiOptions = ({
|
|||
<Featherless apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
|
||||
)}
|
||||
|
||||
{selectedProviderModels.length > 0 && (
|
||||
{selectedProvider !== "xai" && selectedProviderModels.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block font-medium mb-1">{t("settings:providers.model")}</label>
|
||||
|
|
|
|||
|
|
@ -553,6 +553,32 @@ describe("ApiOptions", () => {
|
|||
expect(screen.getByTestId("litellm-refresh-models")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("hides generic Model picker when provider is xai", () => {
|
||||
renderApiOptions({
|
||||
apiConfiguration: {
|
||||
apiProvider: "xai",
|
||||
},
|
||||
})
|
||||
// The generic "Model" label should be absent for xai (uses provider-specific picker)
|
||||
expect(screen.queryByText("Model")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("disables xAI refresh and hides ModelPicker when no API key", () => {
|
||||
renderApiOptions({
|
||||
apiConfiguration: {
|
||||
apiProvider: "xai",
|
||||
xaiApiKey: "",
|
||||
},
|
||||
})
|
||||
// Generic Model picker should be hidden for xAI
|
||||
expect(screen.queryByText("Model")).not.toBeInTheDocument()
|
||||
// If the provider-specific refresh button is present, it should be disabled without a key
|
||||
const btn = screen.queryByTestId("xai-refresh-models")
|
||||
if (btn) {
|
||||
expect(btn).toBeDisabled()
|
||||
}
|
||||
})
|
||||
|
||||
it("does not render LiteLLM component when other provider is selected", () => {
|
||||
renderApiOptions({
|
||||
apiConfiguration: {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
openAiNativeModels,
|
||||
qwenCodeModels,
|
||||
vertexModels,
|
||||
xaiModels,
|
||||
groqModels,
|
||||
sambaNovaModels,
|
||||
doubaoModels,
|
||||
|
|
@ -35,7 +34,6 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
|
|||
"openai-native": openAiNativeModels,
|
||||
"qwen-code": qwenCodeModels,
|
||||
vertex: vertexModels,
|
||||
xai: xaiModels,
|
||||
groq: groqModels,
|
||||
sambanova: sambaNovaModels,
|
||||
zai: internationalZAiModels,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,40 @@
|
|||
import { useCallback } from "react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { ProviderSettings } from "@roo-code/types"
|
||||
import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types"
|
||||
import { xaiDefaultModelId, xaiModels } from "@roo-code/types"
|
||||
|
||||
import type { RouterModels } from "@roo/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
|
||||
import { Button } from "@src/components/ui"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
import { ModelPicker } from "../ModelPicker"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
type XAIProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
|
||||
routerModels?: RouterModels
|
||||
refetchRouterModels?: () => void
|
||||
organizationAllowList?: OrganizationAllowList
|
||||
modelValidationError?: string
|
||||
}
|
||||
|
||||
export const XAI = ({ apiConfiguration, setApiConfigurationField }: XAIProps) => {
|
||||
export const XAI = ({
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
routerModels,
|
||||
refetchRouterModels,
|
||||
organizationAllowList,
|
||||
modelValidationError,
|
||||
}: XAIProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [didRefetch, setDidRefetch] = useState<boolean>()
|
||||
const [refreshError, setRefreshError] = useState<string>()
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ProviderSettings, E>(
|
||||
|
|
@ -27,6 +47,56 @@ export const XAI = ({ apiConfiguration, setApiConfigurationField }: XAIProps) =>
|
|||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
// Reset status and request fresh models
|
||||
setDidRefetch(false)
|
||||
setRefreshError(undefined)
|
||||
|
||||
// Flush xAI cache and request fresh models
|
||||
vscode.postMessage({ type: "flushRouterModels", text: "xai" })
|
||||
vscode.postMessage({ type: "requestRouterModels" })
|
||||
|
||||
// Allow consumer to refetch react-query if provided
|
||||
refetchRouterModels?.()
|
||||
}, [refetchRouterModels])
|
||||
|
||||
// Listen for router responses to determine success/failure
|
||||
useEvent(
|
||||
"message",
|
||||
useCallback(
|
||||
(event: MessageEvent) => {
|
||||
const message: any = event.data
|
||||
// Error channel: single provider failure
|
||||
if (message?.type === "singleRouterModelFetchResponse" && message?.values?.provider === "xai") {
|
||||
if (!message.success) {
|
||||
setDidRefetch(false)
|
||||
setRefreshError(
|
||||
t("settings:providers.refreshModels.error") ||
|
||||
"Failed to fetch xAI models. Please verify your API key and try again.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Success path: routerModels set with non-empty xai models
|
||||
if (message?.type === "routerModels") {
|
||||
const models = message.routerModels?.xai ?? {}
|
||||
if (models && Object.keys(models).length > 0) {
|
||||
setRefreshError(undefined)
|
||||
setDidRefetch(true)
|
||||
} else if (apiConfiguration?.xaiApiKey) {
|
||||
// With a key provided, an empty set indicates failure/unavailable
|
||||
setDidRefetch(false)
|
||||
setRefreshError(
|
||||
t("settings:providers.refreshModels.error") ||
|
||||
"No xAI models found for this API key. Please verify your API key and try again.",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
[apiConfiguration?.xaiApiKey, t],
|
||||
),
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
|
|
@ -45,6 +115,81 @@ export const XAI = ({ apiConfiguration, setApiConfigurationField }: XAIProps) =>
|
|||
{t("settings:providers.getXaiApiKey")}
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
|
||||
{/* Refresh button is disabled without API key */}
|
||||
<div className="flex justify-end mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRefresh}
|
||||
className="w-1/2 max-w-xs"
|
||||
data-testid="xai-refresh-models"
|
||||
disabled={!apiConfiguration?.xaiApiKey}>
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="codicon codicon-refresh" />
|
||||
{t("settings:providers.refreshModels.label")}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Status messaging */}
|
||||
{refreshError && <div className="flex items-center text-vscode-errorForeground mt-2">{refreshError}</div>}
|
||||
{!refreshError && didRefetch && (
|
||||
<div className="flex items-center text-vscode-charts-green mt-2">
|
||||
{t("settings:providers.refreshModels.success")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hide ModelPicker until an API key is provided */}
|
||||
{apiConfiguration?.xaiApiKey ? (
|
||||
<>
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
defaultModelId={xaiDefaultModelId}
|
||||
models={routerModels?.xai ?? {}}
|
||||
modelIdKey="apiModelId"
|
||||
serviceName="xAI (Grok)"
|
||||
serviceUrl="https://api.x.ai/docs"
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
organizationAllowList={organizationAllowList as OrganizationAllowList}
|
||||
errorMessage={modelValidationError}
|
||||
/>
|
||||
|
||||
{/* Context Window Override - only show for models not in static registry or with undefined contextWindow */}
|
||||
{(() => {
|
||||
const selectedModelId = apiConfiguration?.apiModelId || xaiDefaultModelId
|
||||
const staticModel = xaiModels[selectedModelId as keyof typeof xaiModels]
|
||||
const hasStaticContextWindow = staticModel?.contextWindow !== undefined
|
||||
|
||||
if (!hasStaticContextWindow) {
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.xaiModelContextWindow?.toString() || ""}
|
||||
onInput={handleInputChange("xaiModelContextWindow", (e) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const value = target.value
|
||||
return value ? parseInt(value, 10) : undefined
|
||||
})}
|
||||
placeholder="e.g., 256000"
|
||||
className="w-full mt-4">
|
||||
<label className="block font-medium mb-1">
|
||||
Context Window Override (tokens)
|
||||
</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
This model's context window is not known. Please enter it manually.
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm text-vscode-descriptionForeground mt-2">
|
||||
{t("settings:providers.refreshModels.missingConfig")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,8 +182,34 @@ function getSelectedModel({
|
|||
}
|
||||
case "xai": {
|
||||
const id = apiConfiguration.apiModelId ?? xaiDefaultModelId
|
||||
const info = xaiModels[id as keyof typeof xaiModels]
|
||||
return info ? { id, info } : { id, info: undefined }
|
||||
const dynamicInfo = routerModels.xai?.[id]
|
||||
if (dynamicInfo) {
|
||||
// If router-provided model lacks contextWindow, apply manual override when provided
|
||||
const overrideCw = apiConfiguration.xaiModelContextWindow
|
||||
const info =
|
||||
dynamicInfo.contextWindow === undefined && typeof overrideCw === "number"
|
||||
? { ...dynamicInfo, contextWindow: overrideCw }
|
||||
: dynamicInfo
|
||||
return { id, info }
|
||||
}
|
||||
const staticInfo = xaiModels[id as keyof typeof xaiModels]
|
||||
// Build a complete ModelInfo fallback to satisfy UI expectations until dynamic models load
|
||||
const info: ModelInfo = {
|
||||
...openAiModelInfoSaneDefaults,
|
||||
contextWindow:
|
||||
apiConfiguration.xaiModelContextWindow ??
|
||||
staticInfo?.contextWindow ??
|
||||
openAiModelInfoSaneDefaults.contextWindow,
|
||||
maxTokens: staticInfo?.maxTokens ?? openAiModelInfoSaneDefaults.maxTokens,
|
||||
supportsPromptCache: false, // Placeholder; dynamic API will provide real value
|
||||
supportsImages: false, // Placeholder; dynamic API will provide real value
|
||||
description: staticInfo?.description,
|
||||
supportsReasoningEffort:
|
||||
staticInfo && "supportsReasoningEffort" in staticInfo
|
||||
? staticInfo.supportsReasoningEffort
|
||||
: undefined,
|
||||
}
|
||||
return { id, info }
|
||||
}
|
||||
case "groq": {
|
||||
const id = apiConfiguration.apiModelId ?? groqDefaultModelId
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ describe("Model Validation Functions", () => {
|
|||
huggingface: {},
|
||||
roo: {},
|
||||
chutes: {},
|
||||
xai: {},
|
||||
}
|
||||
|
||||
const allowAllOrganization: OrganizationAllowList = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue