From 5ef76c9c22c2a1a7d9d311927a51a6294040ae8c Mon Sep 17 00:00:00 2001 From: siyoon Date: Wed, 5 Aug 2026 15:31:25 +0900 Subject: [PATCH 1/3] feat: add FriendliAI provider with auto-sync from serverless API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds general/friendli.json and pricing/friendli.json generated from the FriendliAI serverless API (https://api.friendli.ai/serverless/v1/models). - general/friendli.json: model capabilities (max_tokens, tools, image) with Friendli-specific params (reasoning controls: chat_template_kwargs, parse_reasoning, include_reasoning) - pricing/friendli.json: per-model pricing (USD/token → cents/token ×100) including cache_read_input_token where available - scripts/sync-friendli-models.js: fetches the live model list and regenerates both JSON files; supports --dry-run; retries on 503 - README.md: add FriendliAI to the providers list Companion to gateway PR #1756 (provider registration as 'friendli'). --- README.md | 2 +- general/friendli.json | 179 ++++++++++++++++++++++++++++ pricing/friendli.json | 126 ++++++++++++++++++++ scripts/README.md | 23 ++++ scripts/sync-friendli-models.js | 203 ++++++++++++++++++++++++++++++++ 5 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 general/friendli.json create mode 100644 pricing/friendli.json create mode 100644 scripts/sync-friendli-models.js diff --git a/README.md b/README.md index a3033d4cf..d48219e7d 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ The easiest way to contribute is to pick an issue with the `good first issue` ta
40+ providers -AI21, Anthropic, Anyscale, Azure AI, Azure OpenAI, AWS Bedrock, Cerebras, Cohere, Dashscope, Deepbricks, DeepInfra, DeepSeek, Fireworks AI, GitHub, Google, Groq, Inference.net, Jina, Lambda, Lemonfox AI, Mistral AI, MonsterAPI, Nebius, Nomic, Novita AI, OpenAI, OpenRouter, Oracle, PaLM, Perplexity AI, Predibase, Reka AI, Sagemaker, Segmind, Stability AI, Together AI, Vertex AI, Workers AI, X.AI, Zhipu +AI21, Anthropic, Anyscale, Azure AI, Azure OpenAI, AWS Bedrock, Cerebras, Cohere, Dashscope, Deepbricks, DeepInfra, DeepSeek, Fireworks AI, FriendliAI, GitHub, Google, Groq, Inference.net, Jina, Lambda, Lemonfox AI, Mistral AI, MonsterAPI, Nebius, Nomic, Novita AI, OpenAI, OpenRouter, Oracle, PaLM, Perplexity AI, Predibase, Reka AI, Sagemaker, Segmind, Stability AI, Together AI, Vertex AI, Workers AI, X.AI, Zhipu
diff --git a/general/friendli.json b/general/friendli.json new file mode 100644 index 000000000..78d8f36a6 --- /dev/null +++ b/general/friendli.json @@ -0,0 +1,179 @@ +{ + "name": "friendli", + "description": "", + "default": { + "params": [ + { + "key": "max_tokens", + "defaultValue": 256, + "minValue": 1, + "maxValue": 4096 + }, + { + "key": "temperature", + "defaultValue": 0.7, + "minValue": 0, + "maxValue": 2 + }, + { + "key": "top_p", + "defaultValue": 1, + "minValue": 0, + "maxValue": 1 + }, + { + "key": "repetition_penalty", + "defaultValue": 1, + "minValue": 0, + "maxValue": 2 + }, + { + "key": "chat_template_kwargs", + "type": "json", + "skipValues": [null, {}] + }, + { + "key": "parse_reasoning", + "defaultValue": false, + "type": "boolean", + "skipValues": [null, false] + }, + { + "key": "include_reasoning", + "defaultValue": false, + "type": "boolean", + "skipValues": [null, false] + }, + { + "key": "stop", + "defaultValue": null, + "type": "array-of-strings", + "skipValues": [null, []] + }, + { + "key": "stream", + "defaultValue": true, + "type": "boolean" + }, + { + "key": "tool_choice", + "type": "non-view-manage-data", + "defaultValue": null, + "options": [ + { + "value": "none", + "name": "None" + }, + { + "value": "auto", + "name": "Auto" + }, + { + "value": "required", + "name": "Required" + }, + { + "value": "custom", + "name": "Custom", + "schema": { + "type": "json" + } + } + ], + "skipValues": [null, []], + "rule": { + "default": { + "condition": "tools", + "then": "auto", + "else": null + } + } + } + ], + "messages": { + "options": ["system", "user", "assistant"] + }, + "type": { + "primary": "chat", + "supported": [] + } + }, + "deepseek-ai/DeepSeek-V3.2": { + "name": "deepseek-ai/DeepSeek-V3.2", + "params": [ + { + "key": "max_tokens", + "maxValue": 163840 + } + ], + "type": { + "primary": "chat", + "supported": ["tools"] + } + }, + "google/gemma-4-31B-it": { + "name": "google/gemma-4-31B-it", + "params": [ + { + "key": "max_tokens", + "maxValue": 262144 + } + ], + "type": { + "primary": "chat", + "supported": ["tools", "image"] + } + }, + "LGAI-EXAONE/K-EXAONE-236B-A23B": { + "name": "LGAI-EXAONE/K-EXAONE-236B-A23B", + "params": [ + { + "key": "max_tokens", + "maxValue": 262144 + } + ], + "type": { + "primary": "chat", + "supported": ["tools"] + } + }, + "MiniMaxAI/MiniMax-M2.5": { + "name": "MiniMaxAI/MiniMax-M2.5", + "params": [ + { + "key": "max_tokens", + "maxValue": 196608 + } + ], + "type": { + "primary": "chat", + "supported": ["tools"] + } + }, + "zai-org/GLM-5.1": { + "name": "zai-org/GLM-5.1", + "params": [ + { + "key": "max_tokens", + "maxValue": 202752 + } + ], + "type": { + "primary": "chat", + "supported": ["tools"] + } + }, + "zai-org/GLM-5.2": { + "name": "zai-org/GLM-5.2", + "params": [ + { + "key": "max_tokens", + "maxValue": 1048576 + } + ], + "type": { + "primary": "chat", + "supported": ["tools"] + } + } +} diff --git a/pricing/friendli.json b/pricing/friendli.json new file mode 100644 index 000000000..dc70040ee --- /dev/null +++ b/pricing/friendli.json @@ -0,0 +1,126 @@ +{ + "default": { + "pricing_config": { + "pay_as_you_go": { + "request_token": { + "price": 0 + }, + "response_token": { + "price": 0 + } + }, + "calculate": { + "request": { + "operation": "multiply", + "operands": [ + { + "value": "input_tokens" + }, + { + "value": "rates.request_token" + } + ] + }, + "response": { + "operation": "multiply", + "operands": [ + { + "value": "output_tokens" + }, + { + "value": "rates.response_token" + } + ] + } + }, + "currency": "USD" + } + }, + "deepseek-ai/DeepSeek-V3.2": { + "pricing_config": { + "pay_as_you_go": { + "request_token": { + "price": 0.00005 + }, + "response_token": { + "price": 0.00015 + }, + "cache_read_input_token": { + "price": 0.000025 + } + } + } + }, + "google/gemma-4-31B-it": { + "pricing_config": { + "pay_as_you_go": { + "request_token": { + "price": 0.000014 + }, + "response_token": { + "price": 0.00004 + } + } + } + }, + "LGAI-EXAONE/K-EXAONE-236B-A23B": { + "pricing_config": { + "pay_as_you_go": { + "request_token": { + "price": 0.00002 + }, + "response_token": { + "price": 0.00008 + }, + "cache_read_input_token": { + "price": 0.00001 + } + } + } + }, + "MiniMaxAI/MiniMax-M2.5": { + "pricing_config": { + "pay_as_you_go": { + "request_token": { + "price": 0.00003 + }, + "response_token": { + "price": 0.00012 + }, + "cache_read_input_token": { + "price": 0.000006 + } + } + } + }, + "zai-org/GLM-5.1": { + "pricing_config": { + "pay_as_you_go": { + "request_token": { + "price": 0.00014 + }, + "response_token": { + "price": 0.00044 + }, + "cache_read_input_token": { + "price": 0.000026 + } + } + } + }, + "zai-org/GLM-5.2": { + "pricing_config": { + "pay_as_you_go": { + "request_token": { + "price": 0.00014 + }, + "response_token": { + "price": 0.00044 + }, + "cache_read_input_token": { + "price": 0.000026 + } + } + } + } +} diff --git a/scripts/README.md b/scripts/README.md index 6d088c799..170824b76 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -19,6 +19,29 @@ Syncs pricing configuration files from this repository to the `gateway-enterpris node scripts/sync-to-gateway.js ../gateway-enterprise-node ``` +## sync-friendli-models.js + +Fetches model metadata from the FriendliAI serverless API (`https://api.friendli.ai/serverless/v1/models`) and regenerates `general/friendli.json` and `pricing/friendli.json`. + +### What It Does + +1. Fetches the live model list from the FriendliAI API +2. Generates `general/friendli.json` with capabilities (max_tokens, tool/image support) and Friendli-specific params (reasoning controls) +3. Generates `pricing/friendli.json` with per-model pricing (USD/token → cents/token ×100) +4. Preserves the existing `default` pricing block if present + +### Manual Usage + +```bash +# Write updated JSON files +node scripts/sync-friendli-models.js + +# Preview without writing +node scripts/sync-friendli-models.js --dry-run +``` + +> **Pricing unit:** The Friendli API returns prices in USD/token. This repo stores prices in cents/token, so the script multiplies by 100. + --- ## GitHub Actions Workflows diff --git a/scripts/sync-friendli-models.js b/scripts/sync-friendli-models.js new file mode 100644 index 000000000..4e4eaff3c --- /dev/null +++ b/scripts/sync-friendli-models.js @@ -0,0 +1,203 @@ +#!/usr/bin/env node + +/** + * Sync FriendliAI model metadata from the Friendli serverless API. + * + * Fetches https://api.friendli.ai/serverless/v1/models and regenerates: + * - general/friendli.json (model capabilities + params) + * - pricing/friendli.json (per-model pricing in cents/token) + * + * Friendli API returns pricing in USD/token; this repo stores cents/token (×100). + * + * Usage: node scripts/sync-friendli-models.js [--dry-run] + */ + +const fs = require('fs'); +const path = require('path'); + +const API_URL = 'https://api.friendli.ai/serverless/v1/models'; +const GENERAL_FILE = path.join(__dirname, '..', 'general', 'friendli.json'); +const PRICING_FILE = path.join(__dirname, '..', 'pricing', 'friendli.json'); + +// ── helpers ────────────────────────────────────────────────────────────── + +/** USD/token → cents/token (×100), rounded to 10 significant decimals to avoid float noise. */ +function usdToCents(usd) { + // ponytail: rounding to 10 sig digits; real prices have ≤8 sig figs so this is lossless + return Number((usd * 100).toPrecision(10)); +} + +function buildCommonParams(maxCompletionTokens) { + return [ + { key: 'max_tokens', defaultValue: 256, minValue: 1, maxValue: maxCompletionTokens }, + { key: 'temperature', defaultValue: 0.7, minValue: 0, maxValue: 2 }, + { key: 'top_p', defaultValue: 1, minValue: 0, maxValue: 1 }, + { key: 'repetition_penalty', defaultValue: 1, minValue: 0, maxValue: 2 }, + + // Friendli-specific reasoning controls (see gateway PR #1756) + { key: 'chat_template_kwargs', type: 'json', skipValues: [null, {}] }, + { key: 'parse_reasoning', defaultValue: false, type: 'boolean', skipValues: [null, false] }, + { key: 'include_reasoning', defaultValue: false, type: 'boolean', skipValues: [null, false] }, + + { key: 'stop', defaultValue: null, type: 'array-of-strings', skipValues: [null, []] }, + { key: 'stream', defaultValue: true, type: 'boolean' }, + { + key: 'tool_choice', + type: 'non-view-manage-data', + defaultValue: null, + options: [ + { value: 'none', name: 'None' }, + { value: 'auto', name: 'Auto' }, + { value: 'required', name: 'Required' }, + { value: 'custom', name: 'Custom', schema: { type: 'json' } }, + ], + skipValues: [null, []], + rule: { default: { condition: 'tools', then: 'auto', else: null } }, + }, + ]; +} + +function buildSupported(inputModalities, functionality) { + const supported = []; + if (functionality?.tool_call) supported.push('tools'); + if (inputModalities?.includes('image')) supported.push('image'); + return supported; +} + +function generateGeneralFile(models) { + const general = { + name: 'friendli', + description: '', + default: { + params: buildCommonParams(4096), + messages: { options: ['system', 'user', 'assistant'] }, + type: { primary: 'chat', supported: [] }, + }, + }; + + for (const model of models) { + const supported = buildSupported(model.input_modalities, model.functionality); + general[model.id] = { + name: model.id, + params: [{ key: 'max_tokens', maxValue: model.max_completion_tokens }], + type: { primary: 'chat', supported }, + }; + } + + return general; +} + +function generatePricingFile(models, existing) { + const pricing = {}; + + // Preserve the default block (used for models without explicit pricing) + pricing['default'] = existing?.['default'] ?? { + pricing_config: { + pay_as_you_go: { + request_token: { price: 0 }, + response_token: { price: 0 }, + }, + calculate: { + request: { + operation: 'multiply', + operands: [{ value: 'input_tokens' }, { value: 'rates.request_token' }], + }, + response: { + operation: 'multiply', + operands: [{ value: 'output_tokens' }, { value: 'rates.response_token' }], + }, + }, + currency: 'USD', + }, + }; + + for (const model of models) { + const p = model.pricing || {}; + const entry = { + pricing_config: { + pay_as_you_go: {}, + }, + }; + + const payg = entry.pricing_config.pay_as_you_go; + if (p.input != null) { + payg.request_token = { price: usdToCents(p.input) }; + } + if (p.output != null) { + payg.response_token = { price: usdToCents(p.output) }; + } + if (p.input_cache_read != null) { + payg.cache_read_input_token = { price: usdToCents(p.input_cache_read) }; + } + + pricing[model.id] = entry; + } + + return pricing; +} + +// ── main ───────────────────────────────────────────────────────────────── + +async function main() { + const dryRun = process.argv.includes('--dry-run'); + + console.log(`Fetching ${API_URL} ...`); + // ponytail: 3 retries with backoff; API occasionally returns 503 + let res; + for (let attempt = 1; attempt <= 3; attempt++) { + res = await fetch(API_URL, { headers: { Accept: 'application/json' } }); + if (res.ok) break; + if (attempt < 3) { + const delay = 1000 * attempt; + console.error(` Attempt ${attempt}: ${res.status} ${res.statusText}, retrying in ${delay}ms...`); + await new Promise((r) => setTimeout(r, delay)); + } + } + if (!res || !res.ok) { + console.error(`API returned ${res?.status} ${res?.statusText} after retries`); + process.exit(1); + } + + const body = await res.json(); + const models = body.data; + if (!Array.isArray(models) || models.length === 0) { + console.error('No models found in API response'); + process.exit(1); + } + + // Sort by id for stable output + models.sort((a, b) => a.id.localeCompare(b.id)); + + console.log(`Found ${models.length} model(s):\n ${models.map((m) => m.id).join('\n ')}\n`); + + let existingPricing = null; + if (fs.existsSync(PRICING_FILE)) { + try { + existingPricing = JSON.parse(fs.readFileSync(PRICING_FILE, 'utf8')); + } catch { + // ignore — will regenerate from scratch + } + } + + const general = generateGeneralFile(models); + const pricing = generatePricingFile(models, existingPricing); + + if (dryRun) { + console.log('--- general/friendli.json (dry run) ---'); + console.log(JSON.stringify(general, null, 2)); + console.log('\n--- pricing/friendli.json (dry run) ---'); + console.log(JSON.stringify(pricing, null, 2)); + return; + } + + fs.writeFileSync(GENERAL_FILE, JSON.stringify(general, null, 2) + '\n'); + console.log(`✅ Wrote ${GENERAL_FILE}`); + + fs.writeFileSync(PRICING_FILE, JSON.stringify(pricing, null, 2) + '\n'); + console.log(`✅ Wrote ${PRICING_FILE}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); From 56d9bc49de40300a7cdd5c13b9d3f6b2c8ffbd42 Mon Sep 17 00:00:00 2001 From: siyoon Date: Wed, 5 Aug 2026 15:37:11 +0900 Subject: [PATCH 2/3] fix: align general/friendli.json schema with Friendli OpenAPI spec Replace the placeholder params with proper OpenAI-compatible params from the Friendli OpenAPI spec (ServerlessChatCompletionBody): - default: max_tokens, temperature, top_p, top_k, frequency_penalty, presence_penalty, repetition_penalty, n, stop, stream - tool-capable models: tool_choice + response_format (json_object/json_schema) Friendli-specific reasoning controls (chat_template_kwargs, parse_reasoning, include_reasoning, reasoning_effort, reasoning_budget) are handled by the gateway provider adapter (PR #1756), not the models repo general config. Source: https://github.com/friendliai/friendli-openapi --- general/friendli.json | 463 ++++++++++++++++++++++++++++++-- scripts/sync-friendli-models.js | 62 ++++- 2 files changed, 485 insertions(+), 40 deletions(-) diff --git a/general/friendli.json b/general/friendli.json index 78d8f36a6..9b8820d27 100644 --- a/general/friendli.json +++ b/general/friendli.json @@ -11,7 +11,7 @@ }, { "key": "temperature", - "defaultValue": 0.7, + "defaultValue": 1, "minValue": 0, "maxValue": 2 }, @@ -22,27 +22,34 @@ "maxValue": 1 }, { - "key": "repetition_penalty", - "defaultValue": 1, + "key": "top_k", + "defaultValue": 0, "minValue": 0, + "maxValue": 2048 + }, + { + "key": "frequency_penalty", + "defaultValue": 0, + "minValue": -2, "maxValue": 2 }, { - "key": "chat_template_kwargs", - "type": "json", - "skipValues": [null, {}] + "key": "presence_penalty", + "defaultValue": 0, + "minValue": -2, + "maxValue": 2 }, { - "key": "parse_reasoning", - "defaultValue": false, - "type": "boolean", - "skipValues": [null, false] + "key": "repetition_penalty", + "defaultValue": 1, + "minValue": 0, + "maxValue": 2 }, { - "key": "include_reasoning", - "defaultValue": false, - "type": "boolean", - "skipValues": [null, false] + "key": "n", + "defaultValue": 1, + "minValue": 1, + "maxValue": 10 }, { "key": "stop", @@ -54,6 +61,22 @@ "key": "stream", "defaultValue": true, "type": "boolean" + } + ], + "messages": { + "options": ["system", "user", "assistant"] + }, + "type": { + "primary": "chat", + "supported": [] + } + }, + "deepseek-ai/DeepSeek-V3.2": { + "name": "deepseek-ai/DeepSeek-V3.2", + "params": [ + { + "key": "max_tokens", + "maxValue": 163840 }, { "key": "tool_choice", @@ -88,22 +111,44 @@ "else": null } } - } - ], - "messages": { - "options": ["system", "user", "assistant"] - }, - "type": { - "primary": "chat", - "supported": [] - } - }, - "deepseek-ai/DeepSeek-V3.2": { - "name": "deepseek-ai/DeepSeek-V3.2", - "params": [ + }, { - "key": "max_tokens", - "maxValue": 163840 + "key": "response_format", + "defaultValue": null, + "options": [ + { + "value": null, + "name": "Text" + }, + { + "value": "json_object", + "name": "JSON Object" + }, + { + "value": "json_schema", + "name": "JSON Schema", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "value": "json_schema" + }, + "json_schema": { + "type": "object" + } + } + }, + "params": { + "key": "json_schema", + "defaultValue": null, + "type": "json", + "skipValues": [null] + } + } + ], + "skipValues": [null], + "type": "string" } ], "type": { @@ -117,6 +162,78 @@ { "key": "max_tokens", "maxValue": 262144 + }, + { + "key": "tool_choice", + "type": "non-view-manage-data", + "defaultValue": null, + "options": [ + { + "value": "none", + "name": "None" + }, + { + "value": "auto", + "name": "Auto" + }, + { + "value": "required", + "name": "Required" + }, + { + "value": "custom", + "name": "Custom", + "schema": { + "type": "json" + } + } + ], + "skipValues": [null, []], + "rule": { + "default": { + "condition": "tools", + "then": "auto", + "else": null + } + } + }, + { + "key": "response_format", + "defaultValue": null, + "options": [ + { + "value": null, + "name": "Text" + }, + { + "value": "json_object", + "name": "JSON Object" + }, + { + "value": "json_schema", + "name": "JSON Schema", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "value": "json_schema" + }, + "json_schema": { + "type": "object" + } + } + }, + "params": { + "key": "json_schema", + "defaultValue": null, + "type": "json", + "skipValues": [null] + } + } + ], + "skipValues": [null], + "type": "string" } ], "type": { @@ -130,6 +247,78 @@ { "key": "max_tokens", "maxValue": 262144 + }, + { + "key": "tool_choice", + "type": "non-view-manage-data", + "defaultValue": null, + "options": [ + { + "value": "none", + "name": "None" + }, + { + "value": "auto", + "name": "Auto" + }, + { + "value": "required", + "name": "Required" + }, + { + "value": "custom", + "name": "Custom", + "schema": { + "type": "json" + } + } + ], + "skipValues": [null, []], + "rule": { + "default": { + "condition": "tools", + "then": "auto", + "else": null + } + } + }, + { + "key": "response_format", + "defaultValue": null, + "options": [ + { + "value": null, + "name": "Text" + }, + { + "value": "json_object", + "name": "JSON Object" + }, + { + "value": "json_schema", + "name": "JSON Schema", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "value": "json_schema" + }, + "json_schema": { + "type": "object" + } + } + }, + "params": { + "key": "json_schema", + "defaultValue": null, + "type": "json", + "skipValues": [null] + } + } + ], + "skipValues": [null], + "type": "string" } ], "type": { @@ -143,6 +332,78 @@ { "key": "max_tokens", "maxValue": 196608 + }, + { + "key": "tool_choice", + "type": "non-view-manage-data", + "defaultValue": null, + "options": [ + { + "value": "none", + "name": "None" + }, + { + "value": "auto", + "name": "Auto" + }, + { + "value": "required", + "name": "Required" + }, + { + "value": "custom", + "name": "Custom", + "schema": { + "type": "json" + } + } + ], + "skipValues": [null, []], + "rule": { + "default": { + "condition": "tools", + "then": "auto", + "else": null + } + } + }, + { + "key": "response_format", + "defaultValue": null, + "options": [ + { + "value": null, + "name": "Text" + }, + { + "value": "json_object", + "name": "JSON Object" + }, + { + "value": "json_schema", + "name": "JSON Schema", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "value": "json_schema" + }, + "json_schema": { + "type": "object" + } + } + }, + "params": { + "key": "json_schema", + "defaultValue": null, + "type": "json", + "skipValues": [null] + } + } + ], + "skipValues": [null], + "type": "string" } ], "type": { @@ -156,6 +417,78 @@ { "key": "max_tokens", "maxValue": 202752 + }, + { + "key": "tool_choice", + "type": "non-view-manage-data", + "defaultValue": null, + "options": [ + { + "value": "none", + "name": "None" + }, + { + "value": "auto", + "name": "Auto" + }, + { + "value": "required", + "name": "Required" + }, + { + "value": "custom", + "name": "Custom", + "schema": { + "type": "json" + } + } + ], + "skipValues": [null, []], + "rule": { + "default": { + "condition": "tools", + "then": "auto", + "else": null + } + } + }, + { + "key": "response_format", + "defaultValue": null, + "options": [ + { + "value": null, + "name": "Text" + }, + { + "value": "json_object", + "name": "JSON Object" + }, + { + "value": "json_schema", + "name": "JSON Schema", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "value": "json_schema" + }, + "json_schema": { + "type": "object" + } + } + }, + "params": { + "key": "json_schema", + "defaultValue": null, + "type": "json", + "skipValues": [null] + } + } + ], + "skipValues": [null], + "type": "string" } ], "type": { @@ -169,6 +502,78 @@ { "key": "max_tokens", "maxValue": 1048576 + }, + { + "key": "tool_choice", + "type": "non-view-manage-data", + "defaultValue": null, + "options": [ + { + "value": "none", + "name": "None" + }, + { + "value": "auto", + "name": "Auto" + }, + { + "value": "required", + "name": "Required" + }, + { + "value": "custom", + "name": "Custom", + "schema": { + "type": "json" + } + } + ], + "skipValues": [null, []], + "rule": { + "default": { + "condition": "tools", + "then": "auto", + "else": null + } + } + }, + { + "key": "response_format", + "defaultValue": null, + "options": [ + { + "value": null, + "name": "Text" + }, + { + "value": "json_object", + "name": "JSON Object" + }, + { + "value": "json_schema", + "name": "JSON Schema", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "value": "json_schema" + }, + "json_schema": { + "type": "object" + } + } + }, + "params": { + "key": "json_schema", + "defaultValue": null, + "type": "json", + "skipValues": [null] + } + } + ], + "skipValues": [null], + "type": "string" } ], "type": { diff --git a/scripts/sync-friendli-models.js b/scripts/sync-friendli-models.js index 4e4eaff3c..a418ff2d1 100644 --- a/scripts/sync-friendli-models.js +++ b/scripts/sync-friendli-models.js @@ -9,6 +9,12 @@ * * Friendli API returns pricing in USD/token; this repo stores cents/token (×100). * + * Param schema follows the Friendli OpenAPI spec (ServerlessChatCompletionBody): + * https://github.com/friendliai/friendli-openapi + * Only standard OpenAI-compatible params are included here. Friendli-specific + * reasoning controls (chat_template_kwargs, parse_reasoning, include_reasoning, + * reasoning_effort, reasoning_budget) are handled by the gateway provider adapter. + * * Usage: node scripts/sync-friendli-models.js [--dry-run] */ @@ -27,20 +33,25 @@ function usdToCents(usd) { return Number((usd * 100).toPrecision(10)); } -function buildCommonParams(maxCompletionTokens) { +// Standard params from Friendli OpenAPI spec (ServerlessChatCompletionBody). +function buildDefaultParams() { return [ - { key: 'max_tokens', defaultValue: 256, minValue: 1, maxValue: maxCompletionTokens }, - { key: 'temperature', defaultValue: 0.7, minValue: 0, maxValue: 2 }, + { key: 'max_tokens', defaultValue: 256, minValue: 1, maxValue: 4096 }, + { key: 'temperature', defaultValue: 1, minValue: 0, maxValue: 2 }, { key: 'top_p', defaultValue: 1, minValue: 0, maxValue: 1 }, + { key: 'top_k', defaultValue: 0, minValue: 0, maxValue: 2048 }, + { key: 'frequency_penalty', defaultValue: 0, minValue: -2, maxValue: 2 }, + { key: 'presence_penalty', defaultValue: 0, minValue: -2, maxValue: 2 }, { key: 'repetition_penalty', defaultValue: 1, minValue: 0, maxValue: 2 }, - - // Friendli-specific reasoning controls (see gateway PR #1756) - { key: 'chat_template_kwargs', type: 'json', skipValues: [null, {}] }, - { key: 'parse_reasoning', defaultValue: false, type: 'boolean', skipValues: [null, false] }, - { key: 'include_reasoning', defaultValue: false, type: 'boolean', skipValues: [null, false] }, - + { key: 'n', defaultValue: 1, minValue: 1, maxValue: 10 }, { key: 'stop', defaultValue: null, type: 'array-of-strings', skipValues: [null, []] }, { key: 'stream', defaultValue: true, type: 'boolean' }, + ]; +} + +// Params for models that support tool calling (from Friendli OpenAPI spec). +function buildToolParams() { + return [ { key: 'tool_choice', type: 'non-view-manage-data', @@ -54,6 +65,28 @@ function buildCommonParams(maxCompletionTokens) { skipValues: [null, []], rule: { default: { condition: 'tools', then: 'auto', else: null } }, }, + { + key: 'response_format', + defaultValue: null, + options: [ + { value: null, name: 'Text' }, + { value: 'json_object', name: 'JSON Object' }, + { + value: 'json_schema', + name: 'JSON Schema', + schema: { + type: 'object', + properties: { + type: { type: 'string', value: 'json_schema' }, + json_schema: { type: 'object' }, + }, + }, + params: { key: 'json_schema', defaultValue: null, type: 'json', skipValues: [null] }, + }, + ], + skipValues: [null], + type: 'string', + }, ]; } @@ -69,7 +102,7 @@ function generateGeneralFile(models) { name: 'friendli', description: '', default: { - params: buildCommonParams(4096), + params: buildDefaultParams(), messages: { options: ['system', 'user', 'assistant'] }, type: { primary: 'chat', supported: [] }, }, @@ -77,9 +110,16 @@ function generateGeneralFile(models) { for (const model of models) { const supported = buildSupported(model.input_modalities, model.functionality); + const params = [{ key: 'max_tokens', maxValue: model.max_completion_tokens }]; + + // Add tool_choice + response_format for models that support tool calling + if (model.functionality?.tool_call) { + params.push(...buildToolParams()); + } + general[model.id] = { name: model.id, - params: [{ key: 'max_tokens', maxValue: model.max_completion_tokens }], + params, type: { primary: 'chat', supported }, }; } From 62be4ec8e245dd66e7914dea18a25ef91a2e83d9 Mon Sep 17 00:00:00 2001 From: siyoon Date: Wed, 5 Aug 2026 15:43:11 +0900 Subject: [PATCH 3/3] fix: use API default_params and only spec-documented ranges - max_tokens: per-model maxValue from API max_completion_tokens (163K-1M), no global maxValue 4096 - temperature/top_k/repetition_penalty/n: defaultValue from API default_params, no fabricated minValue/maxValue - top_p (0-1), frequency_penalty (-2..2), presence_penalty (-2..2), min_p (0-1): ranges from Friendli OpenAPI spec descriptions - Added min_p param (present in API default_params, documented in spec) --- general/friendli.json | 21 +++++++--------- scripts/sync-friendli-models.js | 44 ++++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/general/friendli.json b/general/friendli.json index 9b8820d27..08dea102d 100644 --- a/general/friendli.json +++ b/general/friendli.json @@ -6,14 +6,11 @@ { "key": "max_tokens", "defaultValue": 256, - "minValue": 1, - "maxValue": 4096 + "minValue": 1 }, { "key": "temperature", - "defaultValue": 1, - "minValue": 0, - "maxValue": 2 + "defaultValue": 1 }, { "key": "top_p", @@ -23,9 +20,13 @@ }, { "key": "top_k", + "defaultValue": 0 + }, + { + "key": "min_p", "defaultValue": 0, "minValue": 0, - "maxValue": 2048 + "maxValue": 1 }, { "key": "frequency_penalty", @@ -41,15 +42,11 @@ }, { "key": "repetition_penalty", - "defaultValue": 1, - "minValue": 0, - "maxValue": 2 + "defaultValue": 1 }, { "key": "n", - "defaultValue": 1, - "minValue": 1, - "maxValue": 10 + "defaultValue": 1 }, { "key": "stop", diff --git a/scripts/sync-friendli-models.js b/scripts/sync-friendli-models.js index a418ff2d1..109191a06 100644 --- a/scripts/sync-friendli-models.js +++ b/scripts/sync-friendli-models.js @@ -7,13 +7,12 @@ * - general/friendli.json (model capabilities + params) * - pricing/friendli.json (per-model pricing in cents/token) * - * Friendli API returns pricing in USD/token; this repo stores cents/token (×100). - * - * Param schema follows the Friendli OpenAPI spec (ServerlessChatCompletionBody): + * Parameter ranges follow the Friendli OpenAPI spec (ServerlessChatCompletionBody): * https://github.com/friendliai/friendli-openapi - * Only standard OpenAI-compatible params are included here. Friendli-specific - * reasoning controls (chat_template_kwargs, parse_reasoning, include_reasoning, - * reasoning_effort, reasoning_budget) are handled by the gateway provider adapter. + * Only ranges explicitly documented in the spec descriptions are included. + * Default values come from the API's per-model `default_params` field. + * + * Friendli API returns pricing in USD/token; this repo stores cents/token (×100). * * Usage: node scripts/sync-friendli-models.js [--dry-run] */ @@ -33,17 +32,28 @@ function usdToCents(usd) { return Number((usd * 100).toPrecision(10)); } -// Standard params from Friendli OpenAPI spec (ServerlessChatCompletionBody). -function buildDefaultParams() { +/** + * Build default params from API default_params + OpenAPI spec ranges. + * Only minValue/maxValue that are explicitly documented in the spec description are included: + * - top_p: "Values range from 0.0 (exclusive) to 1.0 (inclusive)" + * - frequency_penalty: "Number between -2.0 and 2.0" + * - presence_penalty: "Number between -2.0 and 2.0" + * - min_p: "Values range from 0.0 (inclusive) to 1.0 (inclusive)" + * Params without documented ranges (temperature, top_k, repetition_penalty, n) get defaultValue only. + */ +function buildDefaultParams(defaultParams) { + const dp = defaultParams || {}; + return [ - { key: 'max_tokens', defaultValue: 256, minValue: 1, maxValue: 4096 }, - { key: 'temperature', defaultValue: 1, minValue: 0, maxValue: 2 }, - { key: 'top_p', defaultValue: 1, minValue: 0, maxValue: 1 }, - { key: 'top_k', defaultValue: 0, minValue: 0, maxValue: 2048 }, + { key: 'max_tokens', defaultValue: 256, minValue: 1 }, + { key: 'temperature', defaultValue: dp.temperature ?? 1 }, + { key: 'top_p', defaultValue: dp.top_p ?? 1, minValue: 0, maxValue: 1 }, + { key: 'top_k', defaultValue: dp.top_k ?? 0 }, + { key: 'min_p', defaultValue: dp.min_p ?? 0, minValue: 0, maxValue: 1 }, { key: 'frequency_penalty', defaultValue: 0, minValue: -2, maxValue: 2 }, { key: 'presence_penalty', defaultValue: 0, minValue: -2, maxValue: 2 }, - { key: 'repetition_penalty', defaultValue: 1, minValue: 0, maxValue: 2 }, - { key: 'n', defaultValue: 1, minValue: 1, maxValue: 10 }, + { key: 'repetition_penalty', defaultValue: dp.repetition_penalty ?? 1 }, + { key: 'n', defaultValue: 1 }, { key: 'stop', defaultValue: null, type: 'array-of-strings', skipValues: [null, []] }, { key: 'stream', defaultValue: true, type: 'boolean' }, ]; @@ -98,11 +108,15 @@ function buildSupported(inputModalities, functionality) { } function generateGeneralFile(models) { + // Use the first model's default_params as the provider-wide default + // (all models currently share the same default_params) + const defaultParams = models[0]?.default_params; + const general = { name: 'friendli', description: '', default: { - params: buildDefaultParams(), + params: buildDefaultParams(defaultParams), messages: { options: ['system', 'user', 'assistant'] }, type: { primary: 'chat', supported: [] }, },