/**
* Cloudflare Workers AI Models Fetcher
*
* Dynamically fetches function-calling capable models from Cloudflare API.
* Uses REST API: GET /accounts/{account_id}/ai/models/search
*
* Models are cached in KV for 24 hours.
*/
const CACHE_KEY = 'cloudflare:models:function-calling';
const CACHE_TTL = 24 * 60 * 60; // 24 hours
/**
* Cloudflare AI model from API
*/
interface CloudflareModel {
id: string;
name: string;
description: string;
task: {
id: string;
name: string;
description: string;
};
properties: Array<{
property_id: string;
value: string | Array<{ unit: string; price: number; currency: string }>;
}>;
created_at: string;
}
/**
* Simplified model for our use
*/
export interface WorkersAIModel {
id: string;
name: string;
description: string;
contextLength: number;
pricing: {
prompt: number;
completion: number;
};
}
/**
* Cached response
*/
export interface ModelsCacheResult {
models: WorkersAIModel[];
cachedAt: string;
fromCache: boolean;
}
/**
* Get property value from model properties array
*/
function getProperty(
properties: CloudflareModel['properties'],
propertyId: string
): string | undefined {
const prop = properties.find(p => p.property_id === propertyId);
if (!prop) return undefined;
return typeof prop.value === 'string' ? prop.value : undefined;
}
/**
* Get pricing from model properties
*/
function getPricing(properties: CloudflareModel['properties']): { prompt: number; completion: number } {
const priceProp = properties.find(p => p.property_id === 'price');
if (!priceProp || !Array.isArray(priceProp.value)) {
return { prompt: 0, completion: 0 };
}
const prices = priceProp.value as Array<{ unit: string; price: number }>;
const inputPrice = prices.find(p => p.unit.includes('input'))?.price || 0;
const outputPrice = prices.find(p => p.unit.includes('output'))?.price || 0;
return { prompt: inputPrice, completion: outputPrice };
}
/**
* Create human-friendly model name from ID
*/
function formatModelName(id: string, description: string): string {
// Extract short name from description or ID
const shortDesc = description.split('.')[0].slice(0, 40);
// Map known model IDs to friendly names
const nameMap: Record<string, string> = {
'@cf/meta/llama-4-scout-17b-16e-instruct': 'Llama 4 Scout 17B',
'@cf/meta/llama-3.3-70b-instruct-fp8-fast': 'Llama 3.3 70B (Fast)',
'@cf/ibm-granite/granite-4.0-h-micro': 'IBM Granite 4.0 Micro',
'@cf/qwen/qwen3-30b-a3b-fp8': 'Qwen 3 30B',
'@cf/mistralai/mistral-small-3.1-24b-instruct': 'Mistral Small 3.1 24B',
'@hf/nousresearch/hermes-2-pro-mistral-7b': 'Hermes 2 Pro 7B',
};
return nameMap[id] || shortDesc;
}
/**
* Fetch function-calling models from Cloudflare API
*
* Requires CF_API_TOKEN env var with Workers AI Read permission
*/
export async function fetchWorkersAIModels(
kv: KVNamespace,
accountId: string,
apiToken?: string,
forceRefresh = false
): Promise<ModelsCacheResult> {
// Check cache first
if (!forceRefresh) {
const cached = await kv.get(CACHE_KEY, 'json') as ModelsCacheResult | null;
if (cached) {
return { ...cached, fromCache: true };
}
}
// If no API token, return empty (will use hardcoded fallback)
if (!apiToken) {
console.log('[CloudflareModels] No API token, using hardcoded models');
return { models: [], cachedAt: new Date().toISOString(), fromCache: false };
}
try {
// Fetch from Cloudflare API
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/models/search`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
}
);
if (!response.ok) {
console.error('[CloudflareModels] API error:', response.status);
return { models: [], cachedAt: new Date().toISOString(), fromCache: false };
}
const data = await response.json() as { result: CloudflareModel[]; success: boolean };
if (!data.success || !data.result) {
console.error('[CloudflareModels] API returned unsuccessful');
return { models: [], cachedAt: new Date().toISOString(), fromCache: false };
}
// Filter for Text Generation models with function_calling property
const functionCallingModels = data.result
.filter(m => {
const isTextGen = m.task?.name === 'Text Generation';
const hasFunctionCalling = m.properties?.some(
p => p.property_id === 'function_calling' && p.value === 'true'
);
return isTextGen && hasFunctionCalling;
})
.map(m => ({
id: m.name, // API uses 'name' field for model ID like @cf/meta/...
name: formatModelName(m.name, m.description),
description: m.description,
contextLength: parseInt(getProperty(m.properties, 'context_window') || '8192', 10),
pricing: getPricing(m.properties),
}))
// Sort by context length (largest first)
.sort((a, b) => b.contextLength - a.contextLength);
const result: ModelsCacheResult = {
models: functionCallingModels,
cachedAt: new Date().toISOString(),
fromCache: false,
};
// Cache result
await kv.put(CACHE_KEY, JSON.stringify(result), {
expirationTtl: CACHE_TTL,
});
console.log(`[CloudflareModels] Cached ${functionCallingModels.length} function-calling models`);
return result;
} catch (error) {
console.error('[CloudflareModels] Fetch error:', error);
return { models: [], cachedAt: new Date().toISOString(), fromCache: false };
}
}
/**
* Hardcoded fallback models (verified Jan 2026)
* Used when API token not available or API fails
*/
export const HARDCODED_FUNCTION_CALLING_MODELS: WorkersAIModel[] = [
{
id: '@cf/meta/llama-4-scout-17b-16e-instruct',
name: 'Llama 4 Scout 17B',
description: 'Meta\'s Llama 4 Scout - 17B params, 16 experts, multimodal',
contextLength: 131000,
pricing: { prompt: 0.27, completion: 0.85 },
},
{
id: '@cf/meta/llama-3.3-70b-instruct-fp8-fast',
name: 'Llama 3.3 70B (Fast)',
description: 'Llama 3.3 70B quantized to fp8, optimized for speed',
contextLength: 24000,
pricing: { prompt: 0.29, completion: 2.25 },
},
{
id: '@cf/ibm-granite/granite-4.0-h-micro',
name: 'IBM Granite 4.0 Micro',
description: 'Industry-leading agentic performance, great for RAG',
contextLength: 131000,
pricing: { prompt: 0.017, completion: 0.11 },
},
{
id: '@cf/qwen/qwen3-30b-a3b-fp8',
name: 'Qwen 3 30B',
description: 'Latest Qwen with advanced reasoning and agent capabilities',
contextLength: 32768,
pricing: { prompt: 0.051, completion: 0.34 },
},
{
id: '@cf/mistralai/mistral-small-3.1-24b-instruct',
name: 'Mistral Small 3.1 24B',
description: 'Vision + 128k context, top-tier text and vision tasks',
contextLength: 128000,
pricing: { prompt: 0.35, completion: 0.56 },
},
{
id: '@hf/nousresearch/hermes-2-pro-mistral-7b',
name: 'Hermes 2 Pro 7B',
description: 'Function calling specialist on Mistral 7B base',
contextLength: 24000,
pricing: { prompt: 0, completion: 0 }, // Free tier
},
];
/**
* Invalidate the models cache
*/
export async function invalidateModelsCache(kv: KVNamespace): Promise<void> {
await kv.delete(CACHE_KEY);
}