mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 03:11:10 +00:00
187 lines
5.5 KiB
TypeScript
187 lines
5.5 KiB
TypeScript
import type { KilocodeModelCatalogEntry } from "openclaw/plugin-sdk/provider-model-shared";
|
|
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
|
|
const log = createSubsystemLogger("kilocode-models");
|
|
|
|
export const KILOCODE_BASE_URL = "https://api.kilo.ai/api/gateway/";
|
|
export const KILOCODE_DEFAULT_MODEL_ID = "kilo/auto";
|
|
export const KILOCODE_DEFAULT_MODEL_REF = `kilocode/${KILOCODE_DEFAULT_MODEL_ID}`;
|
|
export const KILOCODE_DEFAULT_MODEL_NAME = "Kilo Auto";
|
|
|
|
export const KILOCODE_MODEL_CATALOG: KilocodeModelCatalogEntry[] = [
|
|
{
|
|
id: KILOCODE_DEFAULT_MODEL_ID,
|
|
name: KILOCODE_DEFAULT_MODEL_NAME,
|
|
input: ["text", "image"],
|
|
reasoning: true,
|
|
},
|
|
];
|
|
|
|
export const KILOCODE_DEFAULT_CONTEXT_WINDOW = 1000000;
|
|
export const KILOCODE_DEFAULT_MAX_TOKENS = 128000;
|
|
export const KILOCODE_DEFAULT_COST = {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
};
|
|
|
|
export const KILOCODE_MODELS_URL = `${KILOCODE_BASE_URL}models`;
|
|
|
|
const DISCOVERY_TIMEOUT_MS = 5000;
|
|
|
|
interface GatewayModelPricing {
|
|
prompt: string;
|
|
completion: string;
|
|
image?: string;
|
|
request?: string;
|
|
input_cache_read?: string;
|
|
input_cache_write?: string;
|
|
web_search?: string;
|
|
internal_reasoning?: string;
|
|
}
|
|
|
|
interface GatewayModelEntry {
|
|
id: string;
|
|
name: string;
|
|
context_length: number;
|
|
architecture?: {
|
|
input_modalities?: string[];
|
|
output_modalities?: string[];
|
|
};
|
|
top_provider?: {
|
|
max_completion_tokens?: number | null;
|
|
};
|
|
pricing: GatewayModelPricing;
|
|
supported_parameters?: string[];
|
|
}
|
|
|
|
interface GatewayModelsResponse {
|
|
data: GatewayModelEntry[];
|
|
}
|
|
|
|
function toPricePerMillion(perToken: string | undefined): number {
|
|
if (!perToken) {
|
|
return 0;
|
|
}
|
|
const num = Number(perToken);
|
|
if (!Number.isFinite(num) || num < 0) {
|
|
return 0;
|
|
}
|
|
return num * 1_000_000;
|
|
}
|
|
|
|
function parseModality(entry: GatewayModelEntry): Array<"text" | "image"> {
|
|
const modalities = entry.architecture?.input_modalities;
|
|
if (!Array.isArray(modalities)) {
|
|
return ["text"];
|
|
}
|
|
const hasImage = modalities.some((m) => typeof m === "string" && m.toLowerCase() === "image");
|
|
return hasImage ? ["text", "image"] : ["text"];
|
|
}
|
|
|
|
function parseReasoning(entry: GatewayModelEntry): boolean {
|
|
const params = entry.supported_parameters;
|
|
if (!Array.isArray(params)) {
|
|
return false;
|
|
}
|
|
return params.includes("reasoning") || params.includes("include_reasoning");
|
|
}
|
|
|
|
function toModelDefinition(entry: GatewayModelEntry): ModelDefinitionConfig {
|
|
return {
|
|
id: entry.id,
|
|
name: entry.name || entry.id,
|
|
reasoning: parseReasoning(entry),
|
|
input: parseModality(entry),
|
|
cost: {
|
|
input: toPricePerMillion(entry.pricing.prompt),
|
|
output: toPricePerMillion(entry.pricing.completion),
|
|
cacheRead: toPricePerMillion(entry.pricing.input_cache_read),
|
|
cacheWrite: toPricePerMillion(entry.pricing.input_cache_write),
|
|
},
|
|
contextWindow: entry.context_length || KILOCODE_DEFAULT_CONTEXT_WINDOW,
|
|
maxTokens: entry.top_provider?.max_completion_tokens ?? KILOCODE_DEFAULT_MAX_TOKENS,
|
|
};
|
|
}
|
|
|
|
function buildStaticCatalog(): ModelDefinitionConfig[] {
|
|
return KILOCODE_MODEL_CATALOG.map((model) => ({
|
|
id: model.id,
|
|
name: model.name,
|
|
reasoning: model.reasoning,
|
|
input: model.input,
|
|
cost: KILOCODE_DEFAULT_COST,
|
|
contextWindow: model.contextWindow ?? KILOCODE_DEFAULT_CONTEXT_WINDOW,
|
|
maxTokens: model.maxTokens ?? KILOCODE_DEFAULT_MAX_TOKENS,
|
|
}));
|
|
}
|
|
|
|
export async function discoverKilocodeModels(): Promise<ModelDefinitionConfig[]> {
|
|
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
return buildStaticCatalog();
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(KILOCODE_MODELS_URL, {
|
|
headers: { Accept: "application/json" },
|
|
signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
log.warn(`Failed to discover models: HTTP ${response.status}, using static catalog`);
|
|
return buildStaticCatalog();
|
|
}
|
|
|
|
const data = (await response.json()) as GatewayModelsResponse;
|
|
if (!Array.isArray(data.data) || data.data.length === 0) {
|
|
log.warn("No models found from gateway API, using static catalog");
|
|
return buildStaticCatalog();
|
|
}
|
|
|
|
const models: ModelDefinitionConfig[] = [];
|
|
const discoveredIds = new Set<string>();
|
|
|
|
for (const entry of data.data) {
|
|
if (!entry || typeof entry !== "object") {
|
|
continue;
|
|
}
|
|
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
if (!id || discoveredIds.has(id)) {
|
|
continue;
|
|
}
|
|
try {
|
|
models.push(toModelDefinition(entry));
|
|
discoveredIds.add(id);
|
|
} catch (e) {
|
|
log.warn(`Skipping malformed model entry "${id}": ${String(e)}`);
|
|
}
|
|
}
|
|
|
|
const staticModels = buildStaticCatalog();
|
|
for (const staticModel of staticModels) {
|
|
if (!discoveredIds.has(staticModel.id)) {
|
|
models.unshift(staticModel);
|
|
}
|
|
}
|
|
|
|
return models.length > 0 ? models : buildStaticCatalog();
|
|
} catch (error) {
|
|
log.warn(`Discovery failed: ${String(error)}, using static catalog`);
|
|
return buildStaticCatalog();
|
|
}
|
|
}
|
|
|
|
export function buildKilocodeModelDefinition(): ModelDefinitionConfig {
|
|
return {
|
|
id: KILOCODE_DEFAULT_MODEL_ID,
|
|
name: KILOCODE_DEFAULT_MODEL_NAME,
|
|
reasoning: true,
|
|
input: ["text", "image"],
|
|
cost: KILOCODE_DEFAULT_COST,
|
|
contextWindow: KILOCODE_DEFAULT_CONTEXT_WINDOW,
|
|
maxTokens: KILOCODE_DEFAULT_MAX_TOKENS,
|
|
};
|
|
}
|