From 6d6743331c8daa0ce0e315af7dcabd196b17e7e8 Mon Sep 17 00:00:00 2001 From: minpeter Date: Tue, 23 Dec 2025 19:02:25 +0900 Subject: [PATCH 1/6] Add Friendli serverless endpoints provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add provider configuration for Friendli serverless endpoints - Implement auto-generation script (generate-friendli.ts) - Add 11 models: Llama, Qwen, DeepSeek-R1, EXAONE, GLM - Handle TOKEN-based pricing (4 models) and SECOND-based pricing (7 models) - Auto-infer model families and open_weights status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- packages/core/script/generate-friendli.ts | 508 ++++++++++++++++++ .../models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml | 22 + .../Qwen-Qwen3-235B-A22B-Instruct-2507.toml | 22 + .../Qwen-Qwen3-235B-A22B-Thinking-2507.toml | 18 + .../friendli/models/Qwen-Qwen3-30B-A3B.toml | 18 + providers/friendli/models/Qwen-Qwen3-32B.toml | 18 + .../models/deepseek-ai-DeepSeek-R1-0528.toml | 18 + .../models/meta-llama-3.1-8b-instruct.toml | 22 + .../models/meta-llama-3.3-70b-instruct.toml | 22 + ...ma-Llama-4-Maverick-17B-128E-Instruct.toml | 18 + ...-llama-Llama-4-Scout-17B-16E-Instruct.toml | 18 + .../friendli/models/zai-org-GLM-4.6.toml | 18 + providers/friendli/provider.toml | 5 + 13 files changed, 727 insertions(+) create mode 100644 packages/core/script/generate-friendli.ts create mode 100644 providers/friendli/models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml create mode 100644 providers/friendli/models/Qwen-Qwen3-235B-A22B-Instruct-2507.toml create mode 100644 providers/friendli/models/Qwen-Qwen3-235B-A22B-Thinking-2507.toml create mode 100644 providers/friendli/models/Qwen-Qwen3-30B-A3B.toml create mode 100644 providers/friendli/models/Qwen-Qwen3-32B.toml create mode 100644 providers/friendli/models/deepseek-ai-DeepSeek-R1-0528.toml create mode 100644 providers/friendli/models/meta-llama-3.1-8b-instruct.toml create mode 100644 providers/friendli/models/meta-llama-3.3-70b-instruct.toml create mode 100644 providers/friendli/models/meta-llama-Llama-4-Maverick-17B-128E-Instruct.toml create mode 100644 providers/friendli/models/meta-llama-Llama-4-Scout-17B-16E-Instruct.toml create mode 100644 providers/friendli/models/zai-org-GLM-4.6.toml create mode 100644 providers/friendli/provider.toml diff --git a/packages/core/script/generate-friendli.ts b/packages/core/script/generate-friendli.ts new file mode 100644 index 00000000..ec55c0f2 --- /dev/null +++ b/packages/core/script/generate-friendli.ts @@ -0,0 +1,508 @@ +#!/usr/bin/env bun + +import { readdir } from "node:fs/promises"; +import path from "node:path"; +import { z } from "zod"; + +// Friendli API endpoint +const API_ENDPOINT = "https://api.friendli.ai/serverless/v1/models"; + +// Zod schemas for API response validation +const Functionality = z.object({ + tool_call: z.boolean(), + parallel_tool_call: z.boolean(), + structured_output: z.boolean(), +}); + +const Pricing = z.object({ + input: z.number(), + output: z.number(), + response_time: z.number(), + unit_type: z.enum(["TOKEN", "SECOND"]), +}); + +const FriendliModel = z + .object({ + id: z.string(), + name: z.string(), + max_completion_tokens: z.number(), + context_length: z.number(), + functionality: Functionality, + pricing: Pricing, + hugging_face_url: z.string().optional(), + description: z.string().optional(), + license: z.string().optional(), + policy: z.string().optional().nullable(), + created: z.number(), // Unix timestamp + }) + .passthrough(); + +const FriendliResponse = z.object({ + data: z.array(FriendliModel), +}); + +// Family inference patterns +const familyPatterns: [RegExp, string][] = [ + [/llama-3\.3/i, "llama-3.3"], + [/llama-3\.1/i, "llama-3.1"], + [/llama-4/i, "llama-4"], + [/qwen3/i, "qwen3"], + [/deepseek-r1/i, "deepseek-r1"], + [/exaone/i, "exaone"], + [/glm-4/i, "glm-4"], +]; + +function inferFamily(modelId: string, modelName: string): string | undefined { + for (const [pattern, family] of familyPatterns) { + if (pattern.test(modelId) || pattern.test(modelName)) { + return family; + } + } + return undefined; +} + +function extractModelName(fullName: string): string { + // "meta-llama/Llama-3.3-70B-Instruct" -> "Llama 3.3 70B Instruct" + const parts = fullName.split("/"); + const modelName = parts[parts.length - 1]; + return modelName + .replace(/-/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase()); +} + +function isReasoningModel(modelId: string): boolean { + return /deepseek-r1|thinking/i.test(modelId); +} + +function determineOpenWeights( + huggingFaceUrl?: string, + license?: string, +): boolean { + if (!huggingFaceUrl) return false; + // Check for open source licenses + const openLicenses = ["apache", "mit", "llama"]; + return openLicenses.some((l) => + license?.toLowerCase().includes(l), + ); +} + +function formatNumber(n: number): string { + if (n >= 1000) { + // Format with underscores for readability (e.g., 131_072) + return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_"); + } + return n.toString(); +} + +function timestampToDate(timestamp: number): string { + const date = new Date(timestamp * 1000); + return date.toISOString().slice(0, 10); +} + +function getTodayDate(): string { + return new Date().toISOString().slice(0, 10); +} + +interface ExistingModel { + name?: string; + family?: string; + attachment?: boolean; + reasoning?: boolean; + tool_call?: boolean; + structured_output?: boolean; + temperature?: boolean; + knowledge?: string; + release_date?: string; + last_updated?: string; + open_weights?: boolean; + interleaved?: boolean | { field: string }; + status?: string; + cost?: { + input?: number; + output?: number; + reasoning?: number; + cache_read?: number; + cache_write?: number; + }; + limit?: { + context?: number; + input?: number; + output?: number; + }; + modalities?: { + input?: string[]; + output?: string[]; + }; + provider?: { + npm?: string; + api?: string; + }; +} + +async function loadExistingModel( + filePath: string, +): Promise { + try { + const file = Bun.file(filePath); + if (!(await file.exists())) { + return null; + } + const toml = await import(filePath, { with: { type: "toml" } }).then( + (mod) => mod.default, + ); + return toml as ExistingModel; + } catch (e) { + console.warn(`Warning: Failed to parse existing file ${filePath}:`, e); + return null; + } +} + +interface MergedModel { + name: string; + family?: string; + attachment: boolean; + reasoning: boolean; + tool_call: boolean; + structured_output?: boolean; + temperature: boolean; + knowledge?: string; + release_date: string; + last_updated: string; + open_weights: boolean; + interleaved?: boolean | { field: string }; + status?: string; + cost?: { + input: number; + output: number; + }; + limit: { + context: number; + output: number; + }; + modalities: { + input: string[]; + output: string[]; + }; +} + +function mergeModel( + apiModel: z.infer, + existing: ExistingModel | null, +): MergedModel { + const contextTokens = apiModel.context_length; + const outputTokens = apiModel.max_completion_tokens; + + // Determine open_weights from hugging_face_url and license + const openWeights = determineOpenWeights( + apiModel.hugging_face_url, + apiModel.license, + ); + + const merged: MergedModel = { + // Always from API + name: extractModelName(apiModel.name), + attachment: false, // All Friendli models are text-only currently + reasoning: isReasoningModel(apiModel.id), + tool_call: apiModel.functionality.tool_call, + temperature: true, + release_date: timestampToDate(apiModel.created), + last_updated: getTodayDate(), + open_weights: openWeights, + limit: { + context: contextTokens, + output: outputTokens, + }, + modalities: { + input: ["text"], + output: ["text"], + }, + }; + + // structured_output only if true + if (apiModel.functionality.structured_output === true) { + merged.structured_output = true; + } + + // Cost from API - ONLY include if unit_type is TOKEN + if (apiModel.pricing.unit_type === "TOKEN") { + merged.cost = { + input: apiModel.pricing.input, + output: apiModel.pricing.output, + }; + } else { + console.log( + ` Note: ${apiModel.id} uses ${apiModel.pricing.unit_type} pricing - cost section omitted`, + ); + } + + // Preserve from existing OR infer + if (existing?.family) { + merged.family = existing.family; + } else { + const inferred = inferFamily(apiModel.id, apiModel.name); + if (inferred) { + merged.family = inferred; + } + } + + // Preserve manual fields from existing + if (existing?.knowledge) { + merged.knowledge = existing.knowledge; + } + if (existing?.interleaved !== undefined) { + merged.interleaved = existing.interleaved; + } + if (existing?.status !== undefined) { + merged.status = existing.status; + } + + return merged; +} + +function formatToml(model: MergedModel): string { + const lines: string[] = []; + + // Basic fields + lines.push(`name = "${model.name.replace(/"/g, '\\"')}"`); + if (model.family) { + lines.push(`family = "${model.family}"`); + } + lines.push(`attachment = ${model.attachment}`); + lines.push(`reasoning = ${model.reasoning}`); + lines.push(`tool_call = ${model.tool_call}`); + if (model.structured_output !== undefined) { + lines.push(`structured_output = ${model.structured_output}`); + } + lines.push(`temperature = ${model.temperature}`); + if (model.knowledge) { + lines.push(`knowledge = "${model.knowledge}"`); + } + lines.push(`release_date = "${model.release_date}"`); + lines.push(`last_updated = "${model.last_updated}"`); + lines.push(`open_weights = ${model.open_weights}`); + if (model.status) { + lines.push(`status = "${model.status}"`); + } + + // Interleaved section (if present) + if (model.interleaved !== undefined) { + lines.push(""); + if (model.interleaved === true) { + lines.push(`interleaved = true`); + } else if (typeof model.interleaved === "object") { + lines.push(`[interleaved]`); + lines.push(`field = "${model.interleaved.field}"`); + } + } + + // Cost section (only if present) + if (model.cost) { + lines.push(""); + lines.push(`[cost]`); + lines.push(`input = ${model.cost.input}`); + lines.push(`output = ${model.cost.output}`); + } + + // Limit section + lines.push(""); + lines.push(`[limit]`); + lines.push(`context = ${formatNumber(model.limit.context)}`); + lines.push(`output = ${formatNumber(model.limit.output)}`); + + // Modalities section + lines.push(""); + lines.push(`[modalities]`); + lines.push( + `input = [${model.modalities.input.map((m) => `"${m}"`).join(", ")}]`, + ); + lines.push( + `output = [${model.modalities.output.map((m) => `"${m}"`).join(", ")}]`, + ); + + return lines.join("\n") + "\n"; +} + +interface Changes { + field: string; + oldValue: string; + newValue: string; +} + +function detectChanges( + existing: ExistingModel | null, + merged: MergedModel, +): Changes[] { + if (!existing) return []; + + const changes: Changes[] = []; + + const compare = (field: string, oldVal: unknown, newVal: unknown) => { + const oldStr = JSON.stringify(oldVal); + const newStr = JSON.stringify(newVal); + if (oldStr !== newStr) { + changes.push({ + field, + oldValue: formatValue(oldVal), + newValue: formatValue(newVal), + }); + } + }; + + const formatValue = (val: unknown): string => { + if (typeof val === "number") return formatNumber(val); + if (Array.isArray(val)) return `[${val.join(", ")}]`; + if (val === undefined) return "(none)"; + return String(val); + }; + + compare("name", existing.name, merged.name); + compare("family", existing.family, merged.family); + compare("attachment", existing.attachment, merged.attachment); + compare("reasoning", existing.reasoning, merged.reasoning); + compare("tool_call", existing.tool_call, merged.tool_call); + compare( + "structured_output", + existing.structured_output, + merged.structured_output, + ); + compare("open_weights", existing.open_weights, merged.open_weights); + compare("release_date", existing.release_date, merged.release_date); + compare("cost.input", existing.cost?.input, merged.cost?.input); + compare("cost.output", existing.cost?.output, merged.cost?.output); + compare("limit.context", existing.limit?.context, merged.limit.context); + compare("limit.output", existing.limit?.output, merged.limit.output); + compare("modalities.input", existing.modalities?.input, merged.modalities.input); + + return changes; +} + +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + + const modelsDir = path.join( + import.meta.dirname, + "..", + "..", + "..", + "providers", + "friendli", + "models", + ); + + if (dryRun) { + console.log(`[DRY RUN] Fetching Friendli models from API...`); + } else { + console.log(`Fetching Friendli models from API...`); + } + + // Fetch API data + const res = await fetch(API_ENDPOINT); + if (!res.ok) { + console.error(`Failed to fetch API: ${res.status} ${res.statusText}`); + process.exit(1); + } + + const json = await res.json(); + const parsed = FriendliResponse.safeParse(json); + if (!parsed.success) { + console.error("Invalid API response:", parsed.error.errors); + process.exit(1); + } + + const apiModels = parsed.data.data; + + // Get existing files + const existingFiles = new Set(); + try { + const files = await readdir(modelsDir); + for (const file of files) { + if (file.endsWith(".toml")) { + existingFiles.add(file); + } + } + } catch { + // Directory might not exist yet + } + + console.log( + `Found ${apiModels.length} models in API, ${existingFiles.size} existing files\n`, + ); + + // Track API model IDs for orphan detection + const apiModelIds = new Set(); + + let created = 0; + let updated = 0; + let unchanged = 0; + + for (const apiModel of apiModels) { + const safeId = apiModel.id.replace(/\//g, "-"); + const filename = `${safeId}.toml`; + const filePath = path.join(modelsDir, filename); + + apiModelIds.add(filename); + + const existing = await loadExistingModel(filePath); + const merged = mergeModel(apiModel, existing); + const tomlContent = formatToml(merged); + + if (existing === null) { + // New file + created++; + if (dryRun) { + console.log(`[DRY RUN] Would create: ${filename}`); + console.log(` name = "${merged.name}"`); + if (merged.family) { + console.log(` family = "${merged.family}" (inferred)`); + } + console.log(""); + } else { + await Bun.write(filePath, tomlContent); + console.log(`Created: ${filename}`); + } + } else { + // Check for changes + const changes = detectChanges(existing, merged); + + if (changes.length > 0) { + updated++; + if (dryRun) { + console.log(`[DRY RUN] Would update: ${filename}`); + } else { + await Bun.write(filePath, tomlContent); + console.log(`Updated: ${filename}`); + } + for (const change of changes) { + console.log(` ${change.field}: ${change.oldValue} → ${change.newValue}`); + } + console.log(""); + } else { + unchanged++; + } + } + } + + // Check for orphaned files + const orphaned: string[] = []; + for (const file of existingFiles) { + if (!apiModelIds.has(file)) { + orphaned.push(file); + console.log(`Warning: Orphaned file (not in API): ${file}`); + } + } + + // Summary + console.log(""); + if (dryRun) { + console.log( + `Summary: ${created} would be created, ${updated} would be updated, ${unchanged} unchanged, ${orphaned.length} orphaned`, + ); + } else { + console.log( + `Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} orphaned`, + ); + } +} + +await main(); diff --git a/providers/friendli/models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml b/providers/friendli/models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml new file mode 100644 index 00000000..377bbafc --- /dev/null +++ b/providers/friendli/models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml @@ -0,0 +1,22 @@ +name = "EXAONE 4.0.1 32B" +family = "exaone" +attachment = false +reasoning = false +tool_call = true +structured_output = true +temperature = true +release_date = "2025-07-31" +last_updated = "2025-12-23" +open_weights = false + +[cost] +input = 0.6 +output = 1 + +[limit] +context = 131_072 +output = 131_072 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/Qwen-Qwen3-235B-A22B-Instruct-2507.toml b/providers/friendli/models/Qwen-Qwen3-235B-A22B-Instruct-2507.toml new file mode 100644 index 00000000..dd5e2d60 --- /dev/null +++ b/providers/friendli/models/Qwen-Qwen3-235B-A22B-Instruct-2507.toml @@ -0,0 +1,22 @@ +name = "Qwen3 235B A22B Instruct 2507" +family = "qwen3" +attachment = false +reasoning = false +tool_call = true +structured_output = true +temperature = true +release_date = "2025-07-29" +last_updated = "2025-12-23" +open_weights = true + +[cost] +input = 0.2 +output = 0.8 + +[limit] +context = 131_072 +output = 131_072 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/Qwen-Qwen3-235B-A22B-Thinking-2507.toml b/providers/friendli/models/Qwen-Qwen3-235B-A22B-Thinking-2507.toml new file mode 100644 index 00000000..4520b9be --- /dev/null +++ b/providers/friendli/models/Qwen-Qwen3-235B-A22B-Thinking-2507.toml @@ -0,0 +1,18 @@ +name = "Qwen3 235B A22B Thinking 2507" +family = "qwen3" +attachment = false +reasoning = true +tool_call = true +structured_output = true +temperature = true +release_date = "2025-07-29" +last_updated = "2025-12-23" +open_weights = true + +[limit] +context = 131_072 +output = 131_072 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/Qwen-Qwen3-30B-A3B.toml b/providers/friendli/models/Qwen-Qwen3-30B-A3B.toml new file mode 100644 index 00000000..73d20ee6 --- /dev/null +++ b/providers/friendli/models/Qwen-Qwen3-30B-A3B.toml @@ -0,0 +1,18 @@ +name = "Qwen3 30B A3B" +family = "qwen3" +attachment = false +reasoning = false +tool_call = true +structured_output = true +temperature = true +release_date = "2025-06-16" +last_updated = "2025-12-23" +open_weights = true + +[limit] +context = 131_072 +output = 8_000 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/Qwen-Qwen3-32B.toml b/providers/friendli/models/Qwen-Qwen3-32B.toml new file mode 100644 index 00000000..caa6ac0a --- /dev/null +++ b/providers/friendli/models/Qwen-Qwen3-32B.toml @@ -0,0 +1,18 @@ +name = "Qwen3 32B" +family = "qwen3" +attachment = false +reasoning = false +tool_call = true +structured_output = true +temperature = true +release_date = "2025-06-16" +last_updated = "2025-12-23" +open_weights = true + +[limit] +context = 131_072 +output = 8_000 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/deepseek-ai-DeepSeek-R1-0528.toml b/providers/friendli/models/deepseek-ai-DeepSeek-R1-0528.toml new file mode 100644 index 00000000..6ae1f635 --- /dev/null +++ b/providers/friendli/models/deepseek-ai-DeepSeek-R1-0528.toml @@ -0,0 +1,18 @@ +name = "DeepSeek R1 0528" +family = "deepseek-r1" +attachment = false +reasoning = true +tool_call = false +structured_output = true +temperature = true +release_date = "2025-07-11" +last_updated = "2025-12-23" +open_weights = false + +[limit] +context = 163_840 +output = 163_840 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/meta-llama-3.1-8b-instruct.toml b/providers/friendli/models/meta-llama-3.1-8b-instruct.toml new file mode 100644 index 00000000..1107774e --- /dev/null +++ b/providers/friendli/models/meta-llama-3.1-8b-instruct.toml @@ -0,0 +1,22 @@ +name = "Llama 3.1 8B Instruct" +family = "llama-3.1" +attachment = false +reasoning = false +tool_call = true +structured_output = true +temperature = true +release_date = "2024-08-01" +last_updated = "2025-12-23" +open_weights = true + +[cost] +input = 0.1 +output = 0.1 + +[limit] +context = 131_072 +output = 8_000 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/meta-llama-3.3-70b-instruct.toml b/providers/friendli/models/meta-llama-3.3-70b-instruct.toml new file mode 100644 index 00000000..2ebd40f7 --- /dev/null +++ b/providers/friendli/models/meta-llama-3.3-70b-instruct.toml @@ -0,0 +1,22 @@ +name = "Llama 3.3 70B Instruct" +family = "llama-3.3" +attachment = false +reasoning = false +tool_call = true +structured_output = true +temperature = true +release_date = "2024-08-01" +last_updated = "2025-12-23" +open_weights = true + +[cost] +input = 0.6 +output = 0.6 + +[limit] +context = 131_072 +output = 131_072 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/meta-llama-Llama-4-Maverick-17B-128E-Instruct.toml b/providers/friendli/models/meta-llama-Llama-4-Maverick-17B-128E-Instruct.toml new file mode 100644 index 00000000..2f1edca2 --- /dev/null +++ b/providers/friendli/models/meta-llama-Llama-4-Maverick-17B-128E-Instruct.toml @@ -0,0 +1,18 @@ +name = "Llama 4 Maverick 17B 128E Instruct" +family = "llama-4" +attachment = false +reasoning = false +tool_call = false +structured_output = true +temperature = true +release_date = "2025-06-16" +last_updated = "2025-12-23" +open_weights = true + +[limit] +context = 131_072 +output = 8_000 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/meta-llama-Llama-4-Scout-17B-16E-Instruct.toml b/providers/friendli/models/meta-llama-Llama-4-Scout-17B-16E-Instruct.toml new file mode 100644 index 00000000..a715adbf --- /dev/null +++ b/providers/friendli/models/meta-llama-Llama-4-Scout-17B-16E-Instruct.toml @@ -0,0 +1,18 @@ +name = "Llama 4 Scout 17B 16E Instruct" +family = "llama-4" +attachment = false +reasoning = false +tool_call = false +structured_output = true +temperature = true +release_date = "2025-06-16" +last_updated = "2025-12-23" +open_weights = true + +[limit] +context = 131_072 +output = 8_000 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/models/zai-org-GLM-4.6.toml b/providers/friendli/models/zai-org-GLM-4.6.toml new file mode 100644 index 00000000..0a05a513 --- /dev/null +++ b/providers/friendli/models/zai-org-GLM-4.6.toml @@ -0,0 +1,18 @@ +name = "GLM 4.6" +family = "glm-4" +attachment = false +reasoning = false +tool_call = true +structured_output = true +temperature = true +release_date = "2025-10-31" +last_updated = "2025-12-23" +open_weights = true + +[limit] +context = 131_072 +output = 131_072 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/friendli/provider.toml b/providers/friendli/provider.toml new file mode 100644 index 00000000..277b34f7 --- /dev/null +++ b/providers/friendli/provider.toml @@ -0,0 +1,5 @@ +name = "Friendli" +env = ["FRIENDLI_TOKEN"] +npm = "@ai-sdk/openai-compatible" +api = "https://api.friendli.ai/serverless/v1" +doc = "https://friendli.ai/docs/guides/serverless_endpoints/introduction" From e47641da01b0f2110c9cd6d2f1c5a212ebde6965 Mon Sep 17 00:00:00 2001 From: minpeter Date: Tue, 23 Dec 2025 19:06:40 +0900 Subject: [PATCH 2/6] Fix TypeScript type errors in generator scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix possible undefined array access in generate-friendli.ts - Fix undefined type assignments in generate-venice.ts - Use safe array access with .at() and nullish coalescing operators 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- packages/core/script/generate-friendli.ts | 2 +- packages/core/script/generate-venice.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/script/generate-friendli.ts b/packages/core/script/generate-friendli.ts index ec55c0f2..865c10fa 100644 --- a/packages/core/script/generate-friendli.ts +++ b/packages/core/script/generate-friendli.ts @@ -64,7 +64,7 @@ function inferFamily(modelId: string, modelName: string): string | undefined { function extractModelName(fullName: string): string { // "meta-llama/Llama-3.3-70B-Instruct" -> "Llama 3.3 70B Instruct" const parts = fullName.split("/"); - const modelName = parts[parts.length - 1]; + const modelName = parts.at(-1) ?? fullName; return modelName .replace(/-/g, " ") .replace(/\b\w/g, (l) => l.toUpperCase()); diff --git a/packages/core/script/generate-venice.ts b/packages/core/script/generate-venice.ts index 5ac11d62..dc42c6c6 100644 --- a/packages/core/script/generate-venice.ts +++ b/packages/core/script/generate-venice.ts @@ -437,10 +437,10 @@ async function main() { const apiKeyArgIndex = args.findIndex((arg) => arg.startsWith("--api-key")); if (apiKeyArgIndex !== -1) { const arg = args[apiKeyArgIndex]; - if (arg.includes("=")) { - apiKey = arg.split("=")[1]; + if (arg?.includes("=")) { + apiKey = arg.split("=")[1] ?? null; } else if (args[apiKeyArgIndex + 1]) { - apiKey = args[apiKeyArgIndex + 1]; + apiKey = args[apiKeyArgIndex + 1] ?? null; } } From 0c4b20a7ade17f8ff18a87efbeae432605f54a88 Mon Sep 17 00:00:00 2001 From: minpeter Date: Tue, 23 Dec 2025 19:15:14 +0900 Subject: [PATCH 3/6] Refactor API key argument parsing to remove redundant null checks --- packages/core/script/generate-venice.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/script/generate-venice.ts b/packages/core/script/generate-venice.ts index dc42c6c6..5ac11d62 100644 --- a/packages/core/script/generate-venice.ts +++ b/packages/core/script/generate-venice.ts @@ -437,10 +437,10 @@ async function main() { const apiKeyArgIndex = args.findIndex((arg) => arg.startsWith("--api-key")); if (apiKeyArgIndex !== -1) { const arg = args[apiKeyArgIndex]; - if (arg?.includes("=")) { - apiKey = arg.split("=")[1] ?? null; + if (arg.includes("=")) { + apiKey = arg.split("=")[1]; } else if (args[apiKeyArgIndex + 1]) { - apiKey = args[apiKeyArgIndex + 1] ?? null; + apiKey = args[apiKeyArgIndex + 1]; } } From 506ab5646cbc338d7bb9baf27b50391da6da5f27 Mon Sep 17 00:00:00 2001 From: minpeter Date: Tue, 23 Dec 2025 19:17:00 +0900 Subject: [PATCH 4/6] Add Friendli provider with 11 models --- providers/friendli/logo.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 providers/friendli/logo.svg diff --git a/providers/friendli/logo.svg b/providers/friendli/logo.svg new file mode 100644 index 00000000..edc7e570 --- /dev/null +++ b/providers/friendli/logo.svg @@ -0,0 +1,3 @@ + + + From 6a97400dd74d2d0bc79606a1b67bc4798be7d440 Mon Sep 17 00:00:00 2001 From: minpeter Date: Tue, 23 Dec 2025 19:46:11 +0900 Subject: [PATCH 5/6] Update reasoning and open_weights for Friendli models --- packages/core/script/generate-friendli.ts | 32 +++++++++---------- .../models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml | 4 +-- .../friendli/models/Qwen-Qwen3-30B-A3B.toml | 2 +- providers/friendli/models/Qwen-Qwen3-32B.toml | 2 +- .../models/deepseek-ai-DeepSeek-R1-0528.toml | 2 +- ...ma-Llama-4-Maverick-17B-128E-Instruct.toml | 2 +- ...-llama-Llama-4-Scout-17B-16E-Instruct.toml | 2 +- .../friendli/models/zai-org-GLM-4.6.toml | 2 +- 8 files changed, 23 insertions(+), 25 deletions(-) diff --git a/packages/core/script/generate-friendli.ts b/packages/core/script/generate-friendli.ts index 865c10fa..1e33339f 100644 --- a/packages/core/script/generate-friendli.ts +++ b/packages/core/script/generate-friendli.ts @@ -70,20 +70,22 @@ function extractModelName(fullName: string): string { .replace(/\b\w/g, (l) => l.toUpperCase()); } +// TODO: Replace with functionality.parse_reasoning from API when available function isReasoningModel(modelId: string): boolean { - return /deepseek-r1|thinking/i.test(modelId); -} + // Non-reasoning: Llama 3.x Instruct, Qwen3 Instruct + const nonReasoningPatterns = [ + /llama-3\.\d.*instruct/i, + /qwen3.*instruct/i, + ]; + + for (const pattern of nonReasoningPatterns) { + if (pattern.test(modelId)) { + return false; + } + } -function determineOpenWeights( - huggingFaceUrl?: string, - license?: string, -): boolean { - if (!huggingFaceUrl) return false; - // Check for open source licenses - const openLicenses = ["apache", "mit", "llama"]; - return openLicenses.some((l) => - license?.toLowerCase().includes(l), - ); + // Everything else is reasoning or hybrid reasoning + return true; } function formatNumber(n: number): string { @@ -192,11 +194,7 @@ function mergeModel( const contextTokens = apiModel.context_length; const outputTokens = apiModel.max_completion_tokens; - // Determine open_weights from hugging_face_url and license - const openWeights = determineOpenWeights( - apiModel.hugging_face_url, - apiModel.license, - ); + const openWeights = Boolean(apiModel.hugging_face_url); const merged: MergedModel = { // Always from API diff --git a/providers/friendli/models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml b/providers/friendli/models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml index 377bbafc..a61fd84b 100644 --- a/providers/friendli/models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml +++ b/providers/friendli/models/LGAI-EXAONE-EXAONE-4.0.1-32B.toml @@ -1,13 +1,13 @@ name = "EXAONE 4.0.1 32B" family = "exaone" attachment = false -reasoning = false +reasoning = true tool_call = true structured_output = true temperature = true release_date = "2025-07-31" last_updated = "2025-12-23" -open_weights = false +open_weights = true [cost] input = 0.6 diff --git a/providers/friendli/models/Qwen-Qwen3-30B-A3B.toml b/providers/friendli/models/Qwen-Qwen3-30B-A3B.toml index 73d20ee6..2a342b6a 100644 --- a/providers/friendli/models/Qwen-Qwen3-30B-A3B.toml +++ b/providers/friendli/models/Qwen-Qwen3-30B-A3B.toml @@ -1,7 +1,7 @@ name = "Qwen3 30B A3B" family = "qwen3" attachment = false -reasoning = false +reasoning = true tool_call = true structured_output = true temperature = true diff --git a/providers/friendli/models/Qwen-Qwen3-32B.toml b/providers/friendli/models/Qwen-Qwen3-32B.toml index caa6ac0a..7b677f79 100644 --- a/providers/friendli/models/Qwen-Qwen3-32B.toml +++ b/providers/friendli/models/Qwen-Qwen3-32B.toml @@ -1,7 +1,7 @@ name = "Qwen3 32B" family = "qwen3" attachment = false -reasoning = false +reasoning = true tool_call = true structured_output = true temperature = true diff --git a/providers/friendli/models/deepseek-ai-DeepSeek-R1-0528.toml b/providers/friendli/models/deepseek-ai-DeepSeek-R1-0528.toml index 6ae1f635..0e5aa937 100644 --- a/providers/friendli/models/deepseek-ai-DeepSeek-R1-0528.toml +++ b/providers/friendli/models/deepseek-ai-DeepSeek-R1-0528.toml @@ -7,7 +7,7 @@ structured_output = true temperature = true release_date = "2025-07-11" last_updated = "2025-12-23" -open_weights = false +open_weights = true [limit] context = 163_840 diff --git a/providers/friendli/models/meta-llama-Llama-4-Maverick-17B-128E-Instruct.toml b/providers/friendli/models/meta-llama-Llama-4-Maverick-17B-128E-Instruct.toml index 2f1edca2..54599873 100644 --- a/providers/friendli/models/meta-llama-Llama-4-Maverick-17B-128E-Instruct.toml +++ b/providers/friendli/models/meta-llama-Llama-4-Maverick-17B-128E-Instruct.toml @@ -1,7 +1,7 @@ name = "Llama 4 Maverick 17B 128E Instruct" family = "llama-4" attachment = false -reasoning = false +reasoning = true tool_call = false structured_output = true temperature = true diff --git a/providers/friendli/models/meta-llama-Llama-4-Scout-17B-16E-Instruct.toml b/providers/friendli/models/meta-llama-Llama-4-Scout-17B-16E-Instruct.toml index a715adbf..bed69ebe 100644 --- a/providers/friendli/models/meta-llama-Llama-4-Scout-17B-16E-Instruct.toml +++ b/providers/friendli/models/meta-llama-Llama-4-Scout-17B-16E-Instruct.toml @@ -1,7 +1,7 @@ name = "Llama 4 Scout 17B 16E Instruct" family = "llama-4" attachment = false -reasoning = false +reasoning = true tool_call = false structured_output = true temperature = true diff --git a/providers/friendli/models/zai-org-GLM-4.6.toml b/providers/friendli/models/zai-org-GLM-4.6.toml index 0a05a513..b1828fb3 100644 --- a/providers/friendli/models/zai-org-GLM-4.6.toml +++ b/providers/friendli/models/zai-org-GLM-4.6.toml @@ -1,7 +1,7 @@ name = "GLM 4.6" family = "glm-4" attachment = false -reasoning = false +reasoning = true tool_call = true structured_output = true temperature = true From e5c683d830b40ff79b00835864e40fb3f2d78081 Mon Sep 17 00:00:00 2001 From: minpeter Date: Wed, 24 Dec 2025 15:07:05 +0900 Subject: [PATCH 6/6] Update Friendli logo to use currentColor in SVG --- providers/friendli/logo.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/friendli/logo.svg b/providers/friendli/logo.svg index edc7e570..7f59a8d6 100644 --- a/providers/friendli/logo.svg +++ b/providers/friendli/logo.svg @@ -1,3 +1,3 @@ - - + +