mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
e00ef16ee7
* fix(xai): default OAuth login to Grok 4.5 * fix(xai): follow OAuth default model automatically * fix(xai): send OAuth proxy request headers * fix(xai): avoid provider API shadowing * fix(qa): restore current main validation gates --------- Co-authored-by: echeung <echeung@agentsonly.com> Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
307 lines
9.8 KiB
TypeScript
307 lines
9.8 KiB
TypeScript
// Xai provider module implements model/runtime integration.
|
|
import {
|
|
buildLiveModelProviderConfig,
|
|
getCachedLiveProviderModelRows,
|
|
type LiveModelCatalogFetchGuard,
|
|
} from "openclaw/plugin-sdk/provider-catalog-live-runtime";
|
|
import type {
|
|
ModelDefinitionConfig,
|
|
ModelProviderConfig,
|
|
} from "openclaw/plugin-sdk/provider-model-shared";
|
|
import {
|
|
buildXaiCatalogModels,
|
|
resolveXaiCatalogEntry,
|
|
XAI_BASE_URL,
|
|
XAI_DEFAULT_CONTEXT_WINDOW,
|
|
XAI_IMAGE_MODELS,
|
|
XAI_DEFAULT_MAX_TOKENS,
|
|
} from "./model-definitions.js";
|
|
import { XAI_OAUTH_AUTO_MODEL_ID } from "./model-id.js";
|
|
|
|
const PROVIDER_ID = "xai";
|
|
const XAI_MODELS_ENDPOINT = `${XAI_BASE_URL}/models`;
|
|
export const XAI_GROK_OAUTH_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
const XAI_GROK_OAUTH_MODELS_ENDPOINT = `${XAI_GROK_OAUTH_BASE_URL}/models`;
|
|
const XAI_GROK_OAUTH_SETTINGS_ENDPOINT = `${XAI_GROK_OAUTH_BASE_URL}/settings`;
|
|
const XAI_MODELS_CACHE_TTL_MS = 60_000;
|
|
const XAI_GROK_OAUTH_MODELS_CACHE_TTL_MS = 60_000;
|
|
// Composer emits replayable Responses reasoning, but the OAuth catalog omits that capability.
|
|
// Keep it classified here or the stream wrapper will omit encrypted reasoning from replay.
|
|
const XAI_GROK_OAUTH_REASONING_MODEL_IDS = new Set(["grok-composer-2.5-fast"]);
|
|
const XAI_UNKNOWN_MODEL_COST = {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
} satisfies ModelDefinitionConfig["cost"];
|
|
|
|
export function buildXaiProvider(
|
|
api: ModelProviderConfig["api"] = "openai-responses",
|
|
): ModelProviderConfig {
|
|
return {
|
|
baseUrl: XAI_BASE_URL,
|
|
api,
|
|
models: buildXaiCatalogModels(),
|
|
};
|
|
}
|
|
|
|
function buildXaiOAuthFallbackProvider(): ModelProviderConfig {
|
|
return {
|
|
baseUrl: XAI_GROK_OAUTH_BASE_URL,
|
|
api: "openai-responses",
|
|
auth: "oauth",
|
|
models: buildXaiCatalogModels(),
|
|
};
|
|
}
|
|
|
|
function normalizeXaiOAuthModelSelector(value: string): string {
|
|
return value.trim().toLowerCase().replace(/[._]+/g, "-");
|
|
}
|
|
|
|
function resolveXaiOAuthAutoTarget(
|
|
models: readonly ModelDefinitionConfig[],
|
|
preferredModelId: string | undefined,
|
|
): ModelDefinitionConfig | undefined {
|
|
const candidates = models.filter((model) => model.id !== XAI_OAUTH_AUTO_MODEL_ID);
|
|
if (preferredModelId) {
|
|
const exact = candidates.find((model) => model.id === preferredModelId);
|
|
if (exact) {
|
|
return exact;
|
|
}
|
|
const selector = normalizeXaiOAuthModelSelector(preferredModelId);
|
|
const normalizedMatches = candidates.filter(
|
|
(model) => normalizeXaiOAuthModelSelector(model.id) === selector,
|
|
);
|
|
if (normalizedMatches.length === 1) {
|
|
return normalizedMatches[0];
|
|
}
|
|
}
|
|
// Match Grok Build's fallback when its remote default is absent or stale:
|
|
// use the first auth-visible model returned by the provider catalog.
|
|
return candidates[0];
|
|
}
|
|
|
|
function withXaiOAuthAutoModel(
|
|
provider: ModelProviderConfig,
|
|
preferredModelId: string | undefined,
|
|
): ModelProviderConfig {
|
|
const target = resolveXaiOAuthAutoTarget(provider.models, preferredModelId);
|
|
if (!target) {
|
|
return provider;
|
|
}
|
|
return {
|
|
...provider,
|
|
models: [
|
|
{
|
|
...target,
|
|
id: XAI_OAUTH_AUTO_MODEL_ID,
|
|
params: {
|
|
...target.params,
|
|
canonicalModelId: target.id,
|
|
},
|
|
},
|
|
...provider.models.filter((model) => model.id !== XAI_OAUTH_AUTO_MODEL_ID),
|
|
],
|
|
};
|
|
}
|
|
|
|
function readXaiOAuthDefaultModelId(value: unknown): string | undefined {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
return readLiveModelString(value, "default_model");
|
|
}
|
|
|
|
async function fetchXaiOAuthDefaultModelId(params: {
|
|
discoveryApiKey: string;
|
|
fetchGuard?: LiveModelCatalogFetchGuard;
|
|
signal?: AbortSignal;
|
|
}): Promise<string | undefined> {
|
|
try {
|
|
const rows = await getCachedLiveProviderModelRows({
|
|
providerId: PROVIDER_ID,
|
|
endpoint: XAI_GROK_OAUTH_SETTINGS_ENDPOINT,
|
|
discoveryApiKey: params.discoveryApiKey,
|
|
fetchGuard: params.fetchGuard,
|
|
signal: params.signal,
|
|
ttlMs: XAI_GROK_OAUTH_MODELS_CACHE_TTL_MS,
|
|
auditContext: "xai-grok-oauth-settings-discovery",
|
|
cacheKeyParts: [
|
|
PROVIDER_ID,
|
|
"grok-oauth-settings",
|
|
XAI_GROK_OAUTH_SETTINGS_ENDPOINT,
|
|
params.discoveryApiKey,
|
|
],
|
|
readRows: (body) => {
|
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
throw new Error("xAI OAuth settings response must be an object");
|
|
}
|
|
return [body];
|
|
},
|
|
shouldCacheRows: (candidateRows) =>
|
|
readXaiOAuthDefaultModelId(candidateRows[0]) !== undefined,
|
|
});
|
|
return readXaiOAuthDefaultModelId(rows[0]);
|
|
} catch {
|
|
// Remote settings are advisory. Catalog order remains the provider-owned fallback.
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export async function buildLiveXaiProvider(params: {
|
|
apiKey?: string;
|
|
discoveryApiKey?: string;
|
|
fetchGuard?: LiveModelCatalogFetchGuard;
|
|
signal?: AbortSignal;
|
|
}): Promise<ModelProviderConfig> {
|
|
return await buildLiveModelProviderConfig({
|
|
providerId: PROVIDER_ID,
|
|
endpoint: XAI_MODELS_ENDPOINT,
|
|
providerConfig: {
|
|
baseUrl: XAI_BASE_URL,
|
|
api: "openai-responses",
|
|
},
|
|
models: buildXaiCatalogModels(),
|
|
apiKey: params.apiKey,
|
|
discoveryApiKey: params.discoveryApiKey,
|
|
fetchGuard: params.fetchGuard,
|
|
signal: params.signal,
|
|
ttlMs: XAI_MODELS_CACHE_TTL_MS,
|
|
auditContext: "xai-model-discovery",
|
|
});
|
|
}
|
|
|
|
function readLiveModelString(row: unknown, key: string): string | undefined {
|
|
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
return undefined;
|
|
}
|
|
const value = (row as Record<string, unknown>)[key];
|
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
}
|
|
|
|
function readLiveModelPositiveInteger(row: unknown, keys: readonly string[]): number | undefined {
|
|
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
return undefined;
|
|
}
|
|
const record = row as Record<string, unknown>;
|
|
for (const key of keys) {
|
|
const value = record[key];
|
|
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function readLiveModelBoolean(row: unknown, key: string): boolean | undefined {
|
|
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
return undefined;
|
|
}
|
|
const value = (row as Record<string, unknown>)[key];
|
|
return typeof value === "boolean" ? value : undefined;
|
|
}
|
|
|
|
function resolveXaiOauthMetadataFallback(modelId: string) {
|
|
if (modelId === "grok-build") {
|
|
return resolveXaiCatalogEntry("grok-build-0.1");
|
|
}
|
|
return resolveXaiCatalogEntry(modelId);
|
|
}
|
|
|
|
function isXaiOAuthResponsesModel(row: unknown, fallback: ModelDefinitionConfig | undefined) {
|
|
const modelId = readLiveModelString(row, "id") ?? readLiveModelString(row, "model");
|
|
if (modelId && (XAI_IMAGE_MODELS as readonly string[]).includes(modelId)) {
|
|
return false;
|
|
}
|
|
const backend =
|
|
readLiveModelString(row, "api_backend") ??
|
|
readLiveModelString(row, "apiBackend") ??
|
|
readLiveModelString(row, "backend");
|
|
if (backend) {
|
|
const normalizedBackend = backend.toLowerCase();
|
|
return (
|
|
normalizedBackend === "responses" ||
|
|
normalizedBackend === "chat" ||
|
|
normalizedBackend === "language"
|
|
);
|
|
}
|
|
return Boolean(fallback);
|
|
}
|
|
|
|
function buildXaiOauthModelFromLiveRow(row: unknown): ModelDefinitionConfig | undefined {
|
|
const modelId = readLiveModelString(row, "id") ?? readLiveModelString(row, "model");
|
|
if (!modelId) {
|
|
return undefined;
|
|
}
|
|
const fallback = resolveXaiOauthMetadataFallback(modelId);
|
|
if (!isXaiOAuthResponsesModel(row, fallback)) {
|
|
return undefined;
|
|
}
|
|
const contextWindow =
|
|
readLiveModelPositiveInteger(row, ["context_window", "contextWindow"]) ??
|
|
fallback?.contextWindow ??
|
|
XAI_DEFAULT_CONTEXT_WINDOW;
|
|
const maxTokens =
|
|
readLiveModelPositiveInteger(row, ["max_completion_tokens", "maxCompletionTokens"]) ??
|
|
fallback?.maxTokens ??
|
|
XAI_DEFAULT_MAX_TOKENS;
|
|
const supportsReasoningEffort =
|
|
readLiveModelBoolean(row, "supports_reasoning_effort") ??
|
|
readLiveModelBoolean(row, "supportsReasoningEffort");
|
|
const reasoning =
|
|
supportsReasoningEffort === true ||
|
|
fallback?.reasoning === true ||
|
|
XAI_GROK_OAUTH_REASONING_MODEL_IDS.has(modelId);
|
|
|
|
return {
|
|
id: modelId,
|
|
name: readLiveModelString(row, "name") ?? fallback?.name ?? modelId,
|
|
api: "openai-responses",
|
|
baseUrl: XAI_GROK_OAUTH_BASE_URL,
|
|
reasoning,
|
|
input: fallback?.input ?? ["text"],
|
|
cost: fallback?.cost ?? XAI_UNKNOWN_MODEL_COST,
|
|
contextWindow,
|
|
maxTokens,
|
|
...(fallback?.compat ? { compat: fallback.compat } : {}),
|
|
...(fallback?.thinkingLevelMap ? { thinkingLevelMap: fallback.thinkingLevelMap } : {}),
|
|
};
|
|
}
|
|
|
|
export async function buildLiveXaiOAuthProvider(params: {
|
|
discoveryApiKey: string;
|
|
fetchGuard?: LiveModelCatalogFetchGuard;
|
|
signal?: AbortSignal;
|
|
}): Promise<ModelProviderConfig> {
|
|
const fallback = buildXaiOAuthFallbackProvider();
|
|
const [provider, preferredModelId] = await Promise.all([
|
|
buildLiveModelProviderConfig({
|
|
providerId: PROVIDER_ID,
|
|
endpoint: XAI_GROK_OAUTH_MODELS_ENDPOINT,
|
|
providerConfig: {
|
|
baseUrl: fallback.baseUrl,
|
|
api: fallback.api,
|
|
auth: fallback.auth,
|
|
},
|
|
models: fallback.models,
|
|
discoveryApiKey: params.discoveryApiKey,
|
|
fetchGuard: params.fetchGuard,
|
|
signal: params.signal,
|
|
ttlMs: XAI_GROK_OAUTH_MODELS_CACHE_TTL_MS,
|
|
auditContext: "xai-grok-oauth-model-discovery",
|
|
cacheKeyParts: [
|
|
PROVIDER_ID,
|
|
"grok-oauth-model-rows",
|
|
XAI_GROK_OAUTH_MODELS_ENDPOINT,
|
|
params.discoveryApiKey,
|
|
],
|
|
projectRows: (rows) =>
|
|
rows
|
|
.map(buildXaiOauthModelFromLiveRow)
|
|
.filter((model): model is ModelDefinitionConfig => Boolean(model)),
|
|
}),
|
|
fetchXaiOAuthDefaultModelId(params),
|
|
]);
|
|
return withXaiOAuthAutoModel(provider, preferredModelId);
|
|
}
|