Merge cabff49cc2 into 09be5d45d5
This commit is contained in:
commit
fc4b54a258
@ -13,6 +13,7 @@ import {
|
||||
SYNTHETIC_MODEL_CATALOG,
|
||||
} from "./synthetic-models.js";
|
||||
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js";
|
||||
import { discoverXaiModels, XAI_BASE_URL } from "./xai-models.js";
|
||||
|
||||
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
|
||||
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
||||
@ -379,6 +380,15 @@ async function buildVeniceProvider(): Promise<ProviderConfig> {
|
||||
};
|
||||
}
|
||||
|
||||
async function buildXaiProvider(apiKey: string): Promise<ProviderConfig> {
|
||||
const models = await discoverXaiModels(apiKey);
|
||||
return {
|
||||
baseUrl: XAI_BASE_URL,
|
||||
api: "openai-completions",
|
||||
models,
|
||||
};
|
||||
}
|
||||
|
||||
async function buildOllamaProvider(): Promise<ProviderConfig> {
|
||||
const models = await discoverOllamaModels();
|
||||
return {
|
||||
@ -431,6 +441,13 @@ export async function resolveImplicitProviders(params: {
|
||||
providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey };
|
||||
}
|
||||
|
||||
const xaiKey =
|
||||
resolveEnvApiKeyVarName("xai") ??
|
||||
resolveApiKeyFromProfiles({ provider: "xai", store: authStore });
|
||||
if (xaiKey) {
|
||||
providers.xai = { ...(await buildXaiProvider(xaiKey)), apiKey: xaiKey };
|
||||
}
|
||||
|
||||
const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal");
|
||||
if (qwenProfiles.length > 0) {
|
||||
providers["qwen-portal"] = {
|
||||
|
||||
135
src/agents/xai-models.test.ts
Normal file
135
src/agents/xai-models.test.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildXaiModelDefinition,
|
||||
discoverXaiModels,
|
||||
XAI_BASE_URL,
|
||||
XAI_DEFAULT_COST,
|
||||
XAI_DEFAULT_MODEL_ID,
|
||||
XAI_MODEL_CATALOG,
|
||||
} from "./xai-models.js";
|
||||
|
||||
describe("XAI_MODEL_CATALOG", () => {
|
||||
it("contains expected model series", () => {
|
||||
const ids = XAI_MODEL_CATALOG.map((m) => m.id);
|
||||
|
||||
// Grok 4.1 series
|
||||
expect(ids).toContain("grok-4-1-fast-reasoning");
|
||||
expect(ids).toContain("grok-4-1-fast-non-reasoning");
|
||||
|
||||
// Grok 4 series
|
||||
expect(ids).toContain("grok-4-07-09");
|
||||
expect(ids).toContain("grok-4-fast-reasoning");
|
||||
expect(ids).toContain("grok-4-fast-non-reasoning");
|
||||
|
||||
// Grok 3 series
|
||||
expect(ids).toContain("grok-3-beta");
|
||||
expect(ids).toContain("grok-3-mini-beta");
|
||||
|
||||
// Grok 2 series
|
||||
expect(ids).toContain("grok-2-1212");
|
||||
expect(ids).toContain("grok-2-vision-1212");
|
||||
|
||||
// Specialized
|
||||
expect(ids).toContain("grok-code-fast-1");
|
||||
});
|
||||
|
||||
it("marks reasoning models correctly", () => {
|
||||
const reasoningModels = XAI_MODEL_CATALOG.filter((m) => m.reasoning);
|
||||
const reasoningIds = reasoningModels.map((m) => m.id);
|
||||
|
||||
expect(reasoningIds).toContain("grok-4-1-fast-reasoning");
|
||||
expect(reasoningIds).toContain("grok-4-fast-reasoning");
|
||||
expect(reasoningIds).toContain("grok-code-fast-1");
|
||||
|
||||
// Non-reasoning models should not be in this list
|
||||
expect(reasoningIds).not.toContain("grok-4-1-fast-non-reasoning");
|
||||
expect(reasoningIds).not.toContain("grok-3-beta");
|
||||
});
|
||||
|
||||
it("marks vision models with image input", () => {
|
||||
const visionModels = XAI_MODEL_CATALOG.filter((m) => m.input.includes("image"));
|
||||
const visionIds = visionModels.map((m) => m.id);
|
||||
|
||||
expect(visionIds).toContain("grok-4-1-fast-reasoning");
|
||||
expect(visionIds).toContain("grok-4-1-fast-non-reasoning");
|
||||
expect(visionIds).toContain("grok-2-vision-1212");
|
||||
|
||||
// Text-only models should not have image input
|
||||
expect(visionIds).not.toContain("grok-3-beta");
|
||||
expect(visionIds).not.toContain("grok-4-07-09");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildXaiModelDefinition", () => {
|
||||
it("builds a valid ModelDefinitionConfig from catalog entry", () => {
|
||||
const entry = XAI_MODEL_CATALOG.find((m) => m.id === "grok-3-beta")!;
|
||||
const config = buildXaiModelDefinition(entry);
|
||||
|
||||
expect(config.id).toBe("grok-3-beta");
|
||||
expect(config.name).toBe("Grok 3");
|
||||
expect(config.reasoning).toBe(false);
|
||||
expect(config.input).toEqual(["text"]);
|
||||
expect(config.cost).toEqual(XAI_DEFAULT_COST);
|
||||
expect(config.contextWindow).toBe(131072);
|
||||
expect(config.maxTokens).toBe(8192);
|
||||
});
|
||||
|
||||
it("preserves vision input for vision models", () => {
|
||||
const entry = XAI_MODEL_CATALOG.find((m) => m.id === "grok-2-vision-1212")!;
|
||||
const config = buildXaiModelDefinition(entry);
|
||||
|
||||
expect(config.input).toEqual(["text", "image"]);
|
||||
});
|
||||
|
||||
it("preserves reasoning flag for reasoning models", () => {
|
||||
const entry = XAI_MODEL_CATALOG.find((m) => m.id === "grok-4-fast-reasoning")!;
|
||||
const config = buildXaiModelDefinition(entry);
|
||||
|
||||
expect(config.reasoning).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("discoverXaiModels", () => {
|
||||
it("returns static catalog in test environment", async () => {
|
||||
// The function checks for test environment and returns static catalog
|
||||
const models = await discoverXaiModels("test-api-key");
|
||||
|
||||
expect(models.length).toBe(XAI_MODEL_CATALOG.length);
|
||||
expect(models.map((m) => m.id)).toContain("grok-3-beta");
|
||||
expect(models.map((m) => m.id)).toContain("grok-4-1-fast-reasoning");
|
||||
});
|
||||
|
||||
it("returns valid ModelDefinitionConfig objects", async () => {
|
||||
const models = await discoverXaiModels("test-api-key");
|
||||
|
||||
for (const model of models) {
|
||||
expect(model.id).toBeDefined();
|
||||
expect(model.name).toBeDefined();
|
||||
expect(typeof model.reasoning).toBe("boolean");
|
||||
expect(Array.isArray(model.input)).toBe(true);
|
||||
expect(model.cost).toBeDefined();
|
||||
expect(typeof model.contextWindow).toBe("number");
|
||||
expect(typeof model.maxTokens).toBe("number");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("XAI constants", () => {
|
||||
it("has correct base URL", () => {
|
||||
expect(XAI_BASE_URL).toBe("https://api.x.ai/v1");
|
||||
});
|
||||
|
||||
it("has correct default model ID", () => {
|
||||
expect(XAI_DEFAULT_MODEL_ID).toBe("grok-3-beta");
|
||||
});
|
||||
|
||||
it("has zero costs (credit-based)", () => {
|
||||
expect(XAI_DEFAULT_COST).toEqual({
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
213
src/agents/xai-models.ts
Normal file
213
src/agents/xai-models.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import type { ModelDefinitionConfig } from "../config/types.js";
|
||||
|
||||
export const XAI_BASE_URL = "https://api.x.ai/v1";
|
||||
export const XAI_DEFAULT_MODEL_ID = "grok-3-beta";
|
||||
export const XAI_DEFAULT_MODEL_REF = `xai/${XAI_DEFAULT_MODEL_ID}`;
|
||||
|
||||
// xAI pricing varies by model; set to 0 as placeholder
|
||||
export const XAI_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete catalog of xAI (Grok) models.
|
||||
*
|
||||
* This catalog serves as a fallback when the xAI API is unreachable
|
||||
* or when no API key is configured. xAI's /v1/models endpoint requires
|
||||
* authentication, unlike some other providers.
|
||||
*
|
||||
* Models organized by series, newest first.
|
||||
*/
|
||||
export const XAI_MODEL_CATALOG = [
|
||||
// Grok 4.1 Series (latest)
|
||||
{
|
||||
id: "grok-4-1-fast-reasoning",
|
||||
name: "Grok 4.1 Fast Reasoning",
|
||||
reasoning: true,
|
||||
input: ["text", "image"] as const,
|
||||
contextWindow: 2000000,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
{
|
||||
id: "grok-4-1-fast-non-reasoning",
|
||||
name: "Grok 4.1 Fast",
|
||||
reasoning: false,
|
||||
input: ["text", "image"] as const,
|
||||
contextWindow: 2000000,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
|
||||
// Grok 4 Series
|
||||
{
|
||||
id: "grok-4-07-09",
|
||||
name: "Grok 4",
|
||||
reasoning: false,
|
||||
input: ["text"] as const,
|
||||
contextWindow: 256000,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
{
|
||||
id: "grok-4-fast-reasoning",
|
||||
name: "Grok 4 Fast Reasoning",
|
||||
reasoning: true,
|
||||
input: ["text"] as const,
|
||||
contextWindow: 2000000,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
{
|
||||
id: "grok-4-fast-non-reasoning",
|
||||
name: "Grok 4 Fast",
|
||||
reasoning: false,
|
||||
input: ["text"] as const,
|
||||
contextWindow: 2000000,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
|
||||
// Grok 3 Series
|
||||
{
|
||||
id: "grok-3-beta",
|
||||
name: "Grok 3",
|
||||
reasoning: false,
|
||||
input: ["text"] as const,
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
{
|
||||
id: "grok-3-mini-beta",
|
||||
name: "Grok 3 Mini",
|
||||
reasoning: false,
|
||||
input: ["text"] as const,
|
||||
contextWindow: 131072,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
|
||||
// Grok 2 Series
|
||||
{
|
||||
id: "grok-2-1212",
|
||||
name: "Grok 2",
|
||||
reasoning: false,
|
||||
input: ["text"] as const,
|
||||
contextWindow: 32768,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
{
|
||||
id: "grok-2-vision-1212",
|
||||
name: "Grok 2 Vision",
|
||||
reasoning: false,
|
||||
input: ["text", "image"] as const,
|
||||
contextWindow: 32768,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
|
||||
// Specialized
|
||||
{
|
||||
id: "grok-code-fast-1",
|
||||
name: "Grok Code Fast 1",
|
||||
reasoning: true,
|
||||
input: ["text"] as const,
|
||||
contextWindow: 256000,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type XaiCatalogEntry = (typeof XAI_MODEL_CATALOG)[number];
|
||||
|
||||
/**
|
||||
* Build a ModelDefinitionConfig from an xAI catalog entry.
|
||||
*/
|
||||
export function buildXaiModelDefinition(entry: XaiCatalogEntry): ModelDefinitionConfig {
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
reasoning: entry.reasoning,
|
||||
input: [...entry.input],
|
||||
cost: XAI_DEFAULT_COST,
|
||||
contextWindow: entry.contextWindow,
|
||||
maxTokens: entry.maxTokens,
|
||||
};
|
||||
}
|
||||
|
||||
// xAI API response types (OpenAI-compatible format)
|
||||
interface XaiModel {
|
||||
id: string;
|
||||
object: "model";
|
||||
created: number;
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
interface XaiModelsResponse {
|
||||
object: "list";
|
||||
data: XaiModel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover models from xAI API with fallback to static catalog.
|
||||
*
|
||||
* Unlike Venice, xAI's /v1/models endpoint requires authentication,
|
||||
* so discovery only works when an API key is provided.
|
||||
*/
|
||||
export async function discoverXaiModels(apiKey: string): Promise<ModelDefinitionConfig[]> {
|
||||
// Skip API discovery in test environment
|
||||
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
||||
return XAI_MODEL_CATALOG.map(buildXaiModelDefinition);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${XAI_BASE_URL}/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(
|
||||
`[xai-models] Failed to discover models: HTTP ${response.status}, using static catalog`,
|
||||
);
|
||||
return XAI_MODEL_CATALOG.map(buildXaiModelDefinition);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as XaiModelsResponse;
|
||||
if (!Array.isArray(data.data) || data.data.length === 0) {
|
||||
console.warn("[xai-models] No models found from API, using static catalog");
|
||||
return XAI_MODEL_CATALOG.map(buildXaiModelDefinition);
|
||||
}
|
||||
|
||||
// Merge discovered models with catalog metadata
|
||||
const catalogById = new Map<string, XaiCatalogEntry>(XAI_MODEL_CATALOG.map((m) => [m.id, m]));
|
||||
const models: ModelDefinitionConfig[] = [];
|
||||
|
||||
for (const apiModel of data.data) {
|
||||
const catalogEntry = catalogById.get(apiModel.id);
|
||||
if (catalogEntry) {
|
||||
// Use catalog metadata for known models
|
||||
models.push(buildXaiModelDefinition(catalogEntry));
|
||||
} else {
|
||||
// Create definition for newly discovered models not in catalog
|
||||
const idLower = apiModel.id.toLowerCase();
|
||||
const isReasoning =
|
||||
idLower.includes("reasoning") || idLower.includes("think") || idLower.includes("r1");
|
||||
|
||||
const hasVision = idLower.includes("vision");
|
||||
|
||||
models.push({
|
||||
id: apiModel.id,
|
||||
name: apiModel.id, // xAI API only returns id, not display names
|
||||
reasoning: isReasoning,
|
||||
input: hasVision ? ["text", "image"] : ["text"],
|
||||
cost: XAI_DEFAULT_COST,
|
||||
contextWindow: 131072, // Conservative default
|
||||
maxTokens: 8192,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return models.length > 0 ? models : XAI_MODEL_CATALOG.map(buildXaiModelDefinition);
|
||||
} catch (error) {
|
||||
console.warn(`[xai-models] Discovery failed: ${String(error)}, using static catalog`);
|
||||
return XAI_MODEL_CATALOG.map(buildXaiModelDefinition);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user