fix(ci): guard venice model discovery fetch

This commit is contained in:
Peter Steinberger
2026-04-11 03:06:57 +01:00
parent ef5b257c30
commit da1e60a6aa

View File

@@ -1,5 +1,6 @@
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { createSubsystemLogger, retryAsync } from "openclaw/plugin-sdk/runtime-env";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
const log = createSubsystemLogger("venice-models");
@@ -7,6 +8,7 @@ const log = createSubsystemLogger("venice-models");
export const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
export const VENICE_DEFAULT_MODEL_ID = "kimi-k2-5";
export const VENICE_DEFAULT_MODEL_REF = `venice/${VENICE_DEFAULT_MODEL_ID}`;
const VENICE_ALLOWED_HOSTNAMES = ["api.venice.ai"];
export const VENICE_DEFAULT_COST = {
input: 0,
@@ -553,21 +555,28 @@ export async function discoverVeniceModels(
}
try {
const response = await retryAsync(
const { response, release } = await retryAsync(
async () => {
const currentResponse = await fetch(`${VENICE_BASE_URL}/models`, {
const result = await fetchWithSsrFGuard({
url: `${VENICE_BASE_URL}/models`,
signal: AbortSignal.timeout(VENICE_DISCOVERY_TIMEOUT_MS),
headers: {
Accept: "application/json",
init: {
headers: {
Accept: "application/json",
},
},
policy: { allowedHostnames: VENICE_ALLOWED_HOSTNAMES },
auditContext: "venice-model-discovery",
});
const currentResponse = result.response;
if (
!currentResponse.ok &&
VENICE_DISCOVERY_RETRYABLE_HTTP_STATUS.has(currentResponse.status)
) {
await result.release();
throw new VeniceDiscoveryHttpError(currentResponse.status);
}
return currentResponse;
return result;
},
{
attempts: 3,
@@ -579,70 +588,75 @@ export async function discoverVeniceModels(
},
);
if (!response.ok) {
log.warn(`Failed to discover models: HTTP ${response.status}, using static catalog`);
return staticVeniceModelDefinitions();
}
const data = (await response.json()) as VeniceModelsResponse;
if (!Array.isArray(data.data) || data.data.length === 0) {
log.warn("No models found from API, using static catalog");
return staticVeniceModelDefinitions();
}
const catalogById = new Map<string, VeniceCatalogEntry>(
VENICE_MODEL_CATALOG.map((m) => [m.id, m]),
);
const models: ModelDefinitionConfig[] = [];
for (const apiModel of data.data) {
const catalogEntry = catalogById.get(apiModel.id);
const apiMaxTokens = resolveApiMaxCompletionTokens({
apiModel,
knownMaxTokens: catalogEntry?.maxTokens,
});
const apiSupportsTools = resolveApiSupportsTools(apiModel);
if (catalogEntry) {
const definition = buildVeniceModelDefinition(catalogEntry);
if (apiMaxTokens !== undefined) {
definition.maxTokens = apiMaxTokens;
}
if (apiSupportsTools === false) {
definition.compat = {
...definition.compat,
supportsTools: false,
};
}
models.push(definition);
} else {
const apiSpec = apiModel.model_spec;
const lowerModelId = normalizeLowercaseStringOrEmpty(apiModel.id);
const isReasoning =
apiSpec?.capabilities?.supportsReasoning ||
lowerModelId.includes("thinking") ||
lowerModelId.includes("reason") ||
lowerModelId.includes("r1");
const hasVision = apiSpec?.capabilities?.supportsVision === true;
models.push({
id: apiModel.id,
name: apiSpec?.name || apiModel.id,
reasoning: isReasoning,
input: hasVision ? ["text", "image"] : ["text"],
cost: VENICE_DEFAULT_COST,
contextWindow:
normalizePositiveInt(apiSpec?.availableContextTokens) ?? VENICE_DEFAULT_CONTEXT_WINDOW,
maxTokens: apiMaxTokens ?? VENICE_DEFAULT_MAX_TOKENS,
compat: {
supportsUsageInStreaming: false,
...(apiSupportsTools === false ? { supportsTools: false } : {}),
},
});
try {
if (!response.ok) {
log.warn(`Failed to discover models: HTTP ${response.status}, using static catalog`);
return staticVeniceModelDefinitions();
}
}
return models.length > 0 ? models : staticVeniceModelDefinitions();
const data = (await response.json()) as VeniceModelsResponse;
if (!Array.isArray(data.data) || data.data.length === 0) {
log.warn("No models found from API, using static catalog");
return staticVeniceModelDefinitions();
}
const catalogById = new Map<string, VeniceCatalogEntry>(
VENICE_MODEL_CATALOG.map((m) => [m.id, m]),
);
const models: ModelDefinitionConfig[] = [];
for (const apiModel of data.data) {
const catalogEntry = catalogById.get(apiModel.id);
const apiMaxTokens = resolveApiMaxCompletionTokens({
apiModel,
knownMaxTokens: catalogEntry?.maxTokens,
});
const apiSupportsTools = resolveApiSupportsTools(apiModel);
if (catalogEntry) {
const definition = buildVeniceModelDefinition(catalogEntry);
if (apiMaxTokens !== undefined) {
definition.maxTokens = apiMaxTokens;
}
if (apiSupportsTools === false) {
definition.compat = {
...definition.compat,
supportsTools: false,
};
}
models.push(definition);
} else {
const apiSpec = apiModel.model_spec;
const lowerModelId = normalizeLowercaseStringOrEmpty(apiModel.id);
const isReasoning =
apiSpec?.capabilities?.supportsReasoning ||
lowerModelId.includes("thinking") ||
lowerModelId.includes("reason") ||
lowerModelId.includes("r1");
const hasVision = apiSpec?.capabilities?.supportsVision === true;
models.push({
id: apiModel.id,
name: apiSpec?.name || apiModel.id,
reasoning: isReasoning,
input: hasVision ? ["text", "image"] : ["text"],
cost: VENICE_DEFAULT_COST,
contextWindow:
normalizePositiveInt(apiSpec?.availableContextTokens) ??
VENICE_DEFAULT_CONTEXT_WINDOW,
maxTokens: apiMaxTokens ?? VENICE_DEFAULT_MAX_TOKENS,
compat: {
supportsUsageInStreaming: false,
...(apiSupportsTools === false ? { supportsTools: false } : {}),
},
});
}
}
return models.length > 0 ? models : staticVeniceModelDefinitions();
} finally {
await release();
}
} catch (error) {
if (error instanceof VeniceDiscoveryHttpError) {
log.warn(`Failed to discover models: HTTP ${error.status}, using static catalog`);