Simplified custom model flow with canonical models (#6934)
This commit is contained in:
@@ -1259,6 +1259,78 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/provider-catalog": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "get_provider_catalog",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "format",
|
||||
"in": "query",
|
||||
"description": "Filter by provider format (openai, anthropic, ollama)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Provider catalog retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProviderCatalogEntry"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid format parameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/provider-catalog/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "get_provider_catalog_template",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "Provider ID from models.dev",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Provider template retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProviderTemplate"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Provider not found in catalog"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/providers": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -4107,6 +4179,10 @@
|
||||
"base_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"catalog_provider_id": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
@@ -5702,6 +5778,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ModelCapabilities": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tool_call",
|
||||
"reasoning",
|
||||
"attachment",
|
||||
"temperature"
|
||||
],
|
||||
"properties": {
|
||||
"attachment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tool_call": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ModelConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -6003,6 +6102,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ModelTemplate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"context_limit",
|
||||
"capabilities",
|
||||
"deprecated"
|
||||
],
|
||||
"properties": {
|
||||
"capabilities": {
|
||||
"$ref": "#/components/schemas/ModelCapabilities"
|
||||
},
|
||||
"context_limit": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ParseRecipeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -6110,6 +6237,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderCatalogEntry": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"format",
|
||||
"api_url",
|
||||
"model_count",
|
||||
"doc_url",
|
||||
"env_var"
|
||||
],
|
||||
"properties": {
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"doc_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"env_var": {
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"model_count": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderDetails": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -6190,6 +6353,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderTemplate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"format",
|
||||
"api_url",
|
||||
"models",
|
||||
"supports_streaming",
|
||||
"env_var",
|
||||
"doc_url"
|
||||
],
|
||||
"properties": {
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"doc_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"env_var": {
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ModelTemplate"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"supports_streaming": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProviderType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -7952,6 +8157,10 @@
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"catalog_provider_id": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
Generated
+32
-10
@@ -238,6 +238,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -607,6 +608,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -647,6 +649,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -1104,6 +1107,7 @@
|
||||
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
@@ -2737,6 +2741,7 @@
|
||||
"integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^3.0.1",
|
||||
"@inquirer/confirm": "^4.0.1",
|
||||
@@ -3212,6 +3217,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
||||
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -6536,8 +6542,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -6825,6 +6830,7 @@
|
||||
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -6866,6 +6872,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -6876,6 +6883,7 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -7016,6 +7024,7 @@
|
||||
"integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.55.0",
|
||||
"@typescript-eslint/types": "8.55.0",
|
||||
@@ -7401,6 +7410,7 @@
|
||||
"integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.18",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -7649,6 +7659,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7721,6 +7732,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -8261,6 +8273,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -9563,8 +9576,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
@@ -9622,6 +9634,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^24.9.0",
|
||||
@@ -10629,6 +10642,7 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -11112,6 +11126,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -12372,6 +12387,7 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz",
|
||||
"integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -13428,6 +13444,7 @@
|
||||
"integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.31",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
@@ -14670,7 +14687,6 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -14691,6 +14707,7 @@
|
||||
"integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
@@ -17081,6 +17098,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -17182,7 +17200,6 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -17198,7 +17215,6 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -17554,6 +17570,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -17563,6 +17580,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -17584,8 +17602,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
@@ -19502,7 +19519,8 @@
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
@@ -20033,6 +20051,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -20457,6 +20476,7 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -20547,6 +20567,7 @@
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
@@ -21195,6 +21216,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -169,6 +169,7 @@ export type CspMetadata = {
|
||||
export type DeclarativeProviderConfig = {
|
||||
api_key_env?: string;
|
||||
base_url: string;
|
||||
catalog_provider_id?: string | null;
|
||||
description?: string | null;
|
||||
display_name: string;
|
||||
engine: ProviderEngine;
|
||||
@@ -671,6 +672,13 @@ export type MessageMetadata = {
|
||||
userVisible: boolean;
|
||||
};
|
||||
|
||||
export type ModelCapabilities = {
|
||||
attachment: boolean;
|
||||
reasoning: boolean;
|
||||
temperature: boolean;
|
||||
tool_call: boolean;
|
||||
};
|
||||
|
||||
export type ModelConfig = {
|
||||
context_limit?: number | null;
|
||||
max_tokens?: number | null;
|
||||
@@ -767,6 +775,14 @@ export type ModelSettings = {
|
||||
use_mlock?: boolean;
|
||||
};
|
||||
|
||||
export type ModelTemplate = {
|
||||
capabilities: ModelCapabilities;
|
||||
context_limit: number;
|
||||
deprecated: boolean;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ParseRecipeRequest = {
|
||||
content: string;
|
||||
};
|
||||
@@ -819,6 +835,16 @@ export type PromptsListResponse = {
|
||||
prompts: Array<Template>;
|
||||
};
|
||||
|
||||
export type ProviderCatalogEntry = {
|
||||
api_url: string;
|
||||
doc_url: string;
|
||||
env_var: string;
|
||||
format: string;
|
||||
id: string;
|
||||
model_count: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ProviderDetails = {
|
||||
is_configured: boolean;
|
||||
metadata: ProviderMetadata;
|
||||
@@ -862,6 +888,17 @@ export type ProviderMetadata = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ProviderTemplate = {
|
||||
api_url: string;
|
||||
doc_url: string;
|
||||
env_var: string;
|
||||
format: string;
|
||||
id: string;
|
||||
models: Array<ModelTemplate>;
|
||||
name: string;
|
||||
supports_streaming: boolean;
|
||||
};
|
||||
|
||||
export type ProviderType = 'Preferred' | 'Builtin' | 'Declarative' | 'Custom';
|
||||
|
||||
export type ProvidersResponse = {
|
||||
@@ -1435,6 +1472,7 @@ export type UiMetadata = {
|
||||
export type UpdateCustomProviderRequest = {
|
||||
api_key: string;
|
||||
api_url: string;
|
||||
catalog_provider_id?: string | null;
|
||||
display_name: string;
|
||||
engine: string;
|
||||
headers?: {
|
||||
@@ -2470,6 +2508,62 @@ export type SavePromptResponses = {
|
||||
|
||||
export type SavePromptResponse = SavePromptResponses[keyof SavePromptResponses];
|
||||
|
||||
export type GetProviderCatalogData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: {
|
||||
/**
|
||||
* Filter by provider format (openai, anthropic, ollama)
|
||||
*/
|
||||
format?: string | null;
|
||||
};
|
||||
url: '/config/provider-catalog';
|
||||
};
|
||||
|
||||
export type GetProviderCatalogErrors = {
|
||||
/**
|
||||
* Invalid format parameter
|
||||
*/
|
||||
400: unknown;
|
||||
};
|
||||
|
||||
export type GetProviderCatalogResponses = {
|
||||
/**
|
||||
* Provider catalog retrieved successfully
|
||||
*/
|
||||
200: Array<ProviderCatalogEntry>;
|
||||
};
|
||||
|
||||
export type GetProviderCatalogResponse = GetProviderCatalogResponses[keyof GetProviderCatalogResponses];
|
||||
|
||||
export type GetProviderCatalogTemplateData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Provider ID from models.dev
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/config/provider-catalog/{id}';
|
||||
};
|
||||
|
||||
export type GetProviderCatalogTemplateErrors = {
|
||||
/**
|
||||
* Provider not found in catalog
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type GetProviderCatalogTemplateResponses = {
|
||||
/**
|
||||
* Provider template retrieved successfully
|
||||
*/
|
||||
200: ProviderTemplate;
|
||||
};
|
||||
|
||||
export type GetProviderCatalogTemplateResponse = GetProviderCatalogTemplateResponses[keyof GetProviderCatalogTemplateResponses];
|
||||
|
||||
export type ProvidersData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -183,7 +183,8 @@ export function LocalModelSetup({ onSuccess, onCancel }: LocalModelSetupProps) {
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-4xl font-light">Run Locally</h1>
|
||||
<p className="text-text-muted text-base sm:text-lg">
|
||||
Download a model to run Goose entirely on your machine — no API keys, no accounts, completely free and private.
|
||||
Download a model to run Goose entirely on your machine — no API keys, no accounts,
|
||||
completely free and private.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -269,7 +270,12 @@ export function LocalModelSetup({ onSuccess, onCancel }: LocalModelSetupProps) {
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -294,8 +300,12 @@ export function LocalModelSetup({ onSuccess, onCancel }: LocalModelSetupProps) {
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium text-text-default text-sm">{model.id}</span>
|
||||
<span className="text-xs text-text-muted">{formatSize(model.size_bytes)}</span>
|
||||
<span className="font-medium text-text-default text-sm">
|
||||
{model.id}
|
||||
</span>
|
||||
<span className="text-xs text-text-muted">
|
||||
{formatSize(model.size_bytes)}
|
||||
</span>
|
||||
{model.status.state === 'Downloaded' && (
|
||||
<span className="text-xs bg-green-600 text-white px-2 py-0.5 rounded-full">
|
||||
Ready
|
||||
@@ -368,7 +378,8 @@ export function LocalModelSetup({ onSuccess, onCancel }: LocalModelSetupProps) {
|
||||
)}
|
||||
{downloadProgress.eta_seconds != null && downloadProgress.eta_seconds > 0 && (
|
||||
<span>
|
||||
~{downloadProgress.eta_seconds < 60
|
||||
~
|
||||
{downloadProgress.eta_seconds < 60
|
||||
? `${Math.round(downloadProgress.eta_seconds)}s`
|
||||
: `${Math.round(downloadProgress.eta_seconds / 60)}m`}{' '}
|
||||
remaining
|
||||
|
||||
@@ -214,9 +214,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && !searching && (
|
||||
<p className="text-xs text-text-muted">{error}</p>
|
||||
)}
|
||||
{error && !searching && <p className="text-xs text-text-muted">{error}</p>}
|
||||
|
||||
{results.length > 0 && (
|
||||
<div className="space-y-1 max-h-96 overflow-y-auto">
|
||||
@@ -289,9 +287,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
{variant.description && (
|
||||
<span className="text-xs text-text-muted">
|
||||
{variant.description}
|
||||
</span>
|
||||
<span className="text-xs text-text-muted">{variant.description}</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
@@ -323,7 +319,8 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-default mb-2">Direct Download</h4>
|
||||
<p className="text-xs text-text-muted mb-2">
|
||||
Specify a model directly: <code className="bg-background-subtle px-1 rounded">user/repo:quantization</code>
|
||||
Specify a model directly:{' '}
|
||||
<code className="bg-background-subtle px-1 rounded">user/repo:quantization</code>
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
|
||||
@@ -14,12 +14,7 @@ import {
|
||||
} from '../../../api';
|
||||
import { HuggingFaceModelSearch } from './HuggingFaceModelSearch';
|
||||
import { ModelSettingsPanel } from './ModelSettingsPanel';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '../../ui/dialog';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
@@ -273,9 +268,7 @@ export const LocalInferenceSettings = () => {
|
||||
onChange={() => selectModel(model.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className="text-sm font-medium text-text-default">
|
||||
{model.id}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-text-default">{model.id}</span>
|
||||
<span className="text-xs text-text-muted">
|
||||
{formatBytes(model.size_bytes)}
|
||||
</span>
|
||||
@@ -324,9 +317,7 @@ export const LocalInferenceSettings = () => {
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h4 className="text-sm font-medium text-text-default">
|
||||
{model.id}
|
||||
</h4>
|
||||
<h4 className="text-sm font-medium text-text-default">{model.id}</h4>
|
||||
<span className="text-xs text-text-muted">
|
||||
{formatBytes(model.size_bytes)}
|
||||
</span>
|
||||
|
||||
@@ -12,7 +12,14 @@ import {
|
||||
const DEFAULT_SETTINGS: ModelSettings = {
|
||||
context_size: null,
|
||||
max_output_tokens: null,
|
||||
sampling: { type: 'Temperature', temperature: 0.8, top_k: 40, top_p: 0.95, min_p: 0.05, seed: null },
|
||||
sampling: {
|
||||
type: 'Temperature',
|
||||
temperature: 0.8,
|
||||
top_k: 40,
|
||||
top_p: 0.95,
|
||||
min_p: 0.05,
|
||||
seed: null,
|
||||
},
|
||||
repeat_penalty: 1.0,
|
||||
repeat_last_n: 64,
|
||||
frequency_penalty: 0.0,
|
||||
@@ -177,7 +184,14 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
|
||||
} else if (type === 'MirostatV2') {
|
||||
sampling = { type: 'MirostatV2', tau: 5.0, eta: 0.1, seed: null };
|
||||
} else {
|
||||
sampling = { type: 'Temperature', temperature: 0.8, top_k: 40, top_p: 0.95, min_p: 0.05, seed: null };
|
||||
sampling = {
|
||||
type: 'Temperature',
|
||||
temperature: 0.8,
|
||||
top_k: 40,
|
||||
top_p: 0.95,
|
||||
min_p: 0.05,
|
||||
seed: null,
|
||||
};
|
||||
}
|
||||
save({ ...settings, sampling });
|
||||
};
|
||||
@@ -391,7 +405,13 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
|
||||
<SelectField
|
||||
label="Flash attention"
|
||||
description="Enable flash attention optimization"
|
||||
value={settings.flash_attention === null || settings.flash_attention === undefined ? 'auto' : settings.flash_attention ? 'on' : 'off'}
|
||||
value={
|
||||
settings.flash_attention === null || settings.flash_attention === undefined
|
||||
? 'auto'
|
||||
: settings.flash_attention
|
||||
? 'on'
|
||||
: 'off'
|
||||
}
|
||||
options={[
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'on', label: 'On' },
|
||||
|
||||
@@ -38,8 +38,8 @@ const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onCli
|
||||
<div className="flex flex-col items-center justify-center min-h-[200px]">
|
||||
<Plus className="w-8 h-8 text-gray-400 mb-2" />
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 text-center">
|
||||
<div>Add</div>
|
||||
<div>Custom Provider</div>
|
||||
<div className="font-medium">Add Provider</div>
|
||||
<div className="text-xs text-gray-500 mt-1">From template or manual setup</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -239,6 +239,7 @@ function ProviderCards({
|
||||
supports_streaming: editingProvider.config.supports_streaming ?? true,
|
||||
requires_auth: editingProvider.config.requires_auth ?? true,
|
||||
headers: editingProvider.config.headers ?? undefined,
|
||||
catalog_provider_id: editingProvider.config.catalog_provider_id ?? undefined,
|
||||
};
|
||||
|
||||
const editable = editingProvider ? editingProvider.isEditable : true;
|
||||
@@ -262,7 +263,7 @@ function ProviderCards({
|
||||
isActiveProvider={isActiveProvider}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>{' '}
|
||||
</Dialog>
|
||||
{configuringProvider && (
|
||||
<ProviderConfigurationModal
|
||||
provider={configuringProvider}
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '../../../../ui/button';
|
||||
import { Search, ExternalLink, Check } from 'lucide-react';
|
||||
import { Input } from '../../../../ui/input';
|
||||
import { Select } from '../../../../ui/Select';
|
||||
import {
|
||||
getProviderCatalog,
|
||||
getProviderCatalogTemplate,
|
||||
type ProviderCatalogEntry,
|
||||
type ProviderTemplate,
|
||||
} from '../../../../../api';
|
||||
|
||||
interface ProviderCatalogPickerProps {
|
||||
onSelect: (template: ProviderTemplate) => void;
|
||||
onCancel: () => void;
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export default function ProviderCatalogPicker({
|
||||
onSelect,
|
||||
onCancel,
|
||||
embedded,
|
||||
}: ProviderCatalogPickerProps) {
|
||||
const [selectedFormat, setSelectedFormat] = useState<string>('openai');
|
||||
const [providers, setProviders] = useState<ProviderCatalogEntry[]>([]);
|
||||
const [filteredProviders, setFilteredProviders] = useState<ProviderCatalogEntry[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const formatOptions = [
|
||||
{ value: 'openai', label: 'OpenAI Compatible' },
|
||||
{ value: 'anthropic', label: 'Anthropic Compatible' },
|
||||
];
|
||||
|
||||
// Fetch providers when format changes
|
||||
useEffect(() => {
|
||||
fetchProviders(selectedFormat);
|
||||
}, [selectedFormat]);
|
||||
|
||||
// Filter providers based on search query
|
||||
useEffect(() => {
|
||||
if (searchQuery.trim() === '') {
|
||||
setFilteredProviders(providers);
|
||||
} else {
|
||||
const query = searchQuery.toLowerCase();
|
||||
setFilteredProviders(
|
||||
providers.filter(
|
||||
(p) => p.name.toLowerCase().includes(query) || p.id.toLowerCase().includes(query)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [searchQuery, providers]);
|
||||
|
||||
const fetchProviders = async (format: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { data } = await getProviderCatalog({
|
||||
query: { format },
|
||||
throwOnError: true,
|
||||
});
|
||||
setProviders(data || []);
|
||||
setFilteredProviders(data || []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProviderSelect = async (providerId: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { data: template } = await getProviderCatalogTemplate({
|
||||
path: { id: providerId },
|
||||
throwOnError: true,
|
||||
});
|
||||
if (template) {
|
||||
onSelect(template);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-textStandard mb-2">Choose Provider</h3>
|
||||
<p className="text-sm text-textSubtle">
|
||||
Select an API format and provider. We'll auto-fill the configuration for you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Format Selection */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-textStandard mb-2 block">API Format</label>
|
||||
<Select
|
||||
options={formatOptions}
|
||||
value={formatOptions.find((opt) => opt.value === selectedFormat)}
|
||||
onChange={(option: unknown) => {
|
||||
const selectedOption = option as { value: string; label: string } | null;
|
||||
if (selectedOption && selectedOption.value) {
|
||||
setSelectedFormat(selectedOption.value);
|
||||
}
|
||||
}}
|
||||
isSearchable={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-textSubtle w-4 h-4" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search providers..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Loading/Error */}
|
||||
{loading && <div className="text-center py-8 text-textSubtle">Loading providers...</div>}
|
||||
{error && <div className="text-center py-8 text-red-500">Error: {error}</div>}
|
||||
|
||||
{/* Provider List */}
|
||||
{!loading && !error && (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{filteredProviders.length === 0 ? (
|
||||
<div className="text-center py-8 text-textSubtle">
|
||||
{searchQuery ? `No providers found for "${searchQuery}"` : 'No providers available'}
|
||||
</div>
|
||||
) : (
|
||||
filteredProviders.map((provider) => (
|
||||
<button
|
||||
key={provider.id}
|
||||
onClick={() => handleProviderSelect(provider.id)}
|
||||
className="w-full p-4 text-left border border-border rounded-lg hover:bg-surfaceHover hover:border-primary transition-colors group"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-medium text-textStandard">{provider.name}</div>
|
||||
{provider.doc_url && (
|
||||
<a
|
||||
href={provider.doc_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-textSubtle hover:text-textStandard transition-colors flex-shrink-0"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-textSubtle mt-1 break-all">{provider.api_url}</div>
|
||||
<div className="text-xs text-textSubtle mt-2">
|
||||
{provider.model_count} models available
|
||||
{provider.env_var && ` • Requires ${provider.env_var}`}
|
||||
</div>
|
||||
</div>
|
||||
<Check className="w-5 h-5 text-primary opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" />
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
{!embedded && (
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+412
-226
@@ -3,9 +3,12 @@ import { Input } from '../../../../../ui/input';
|
||||
import { Select } from '../../../../../ui/Select';
|
||||
import { Button } from '../../../../../ui/button';
|
||||
import { SecureStorageNotice } from '../SecureStorageNotice';
|
||||
import { UpdateCustomProviderRequest } from '../../../../../../api';
|
||||
import { Plus, X, Trash2, AlertTriangle } from 'lucide-react';
|
||||
import { UpdateCustomProviderRequest, type ProviderTemplate } from '../../../../../../api';
|
||||
import { Plus, X, Trash2, AlertTriangle, ExternalLink, Search, Settings } from 'lucide-react';
|
||||
import { cn } from '../../../../../../utils';
|
||||
import ProviderCatalogPicker from '../ProviderCatalogPicker';
|
||||
|
||||
type Step = 'choice' | 'catalog' | 'form';
|
||||
|
||||
interface CustomProviderFormProps {
|
||||
onSubmit: (data: UpdateCustomProviderRequest) => void;
|
||||
@@ -29,7 +32,7 @@ export default function CustomProviderForm({
|
||||
const [apiUrl, setApiUrl] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [models, setModels] = useState('');
|
||||
const [requiresApiKey, setRequiresApiKey] = useState(false);
|
||||
const [requiresAuth, setRequiresAuth] = useState(false);
|
||||
const [supportsStreaming, setSupportsStreaming] = useState(true);
|
||||
const [headers, setHeaders] = useState<{ key: string; value: string }[]>([]);
|
||||
const [newHeaderKey, setNewHeaderKey] = useState('');
|
||||
@@ -42,6 +45,10 @@ export default function CustomProviderForm({
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||
|
||||
// Template + step state
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<ProviderTemplate | null>(null);
|
||||
const [step, setStep] = useState<Step>(initialData ? 'form' : 'choice');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
const engineMap: Record<string, string> = {
|
||||
@@ -54,7 +61,7 @@ export default function CustomProviderForm({
|
||||
setApiUrl(initialData.api_url);
|
||||
setModels(initialData.models.join(', '));
|
||||
setSupportsStreaming(initialData.supports_streaming ?? true);
|
||||
setRequiresApiKey(initialData.requires_auth ?? true);
|
||||
setRequiresAuth(initialData.requires_auth ?? true);
|
||||
|
||||
if (initialData.headers) {
|
||||
const headerList = Object.entries(initialData.headers).map(([key, value]) => ({
|
||||
@@ -63,11 +70,46 @@ export default function CustomProviderForm({
|
||||
}));
|
||||
setHeaders(headerList);
|
||||
}
|
||||
|
||||
setStep('form');
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleRequiresApiKeyChange = (checked: boolean) => {
|
||||
setRequiresApiKey(checked);
|
||||
const handleTemplateSelect = (template: ProviderTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
|
||||
// Prefill fields from template
|
||||
setDisplayName(template.name);
|
||||
setApiUrl(template.api_url);
|
||||
setSupportsStreaming(template.supports_streaming);
|
||||
setRequiresAuth(true);
|
||||
|
||||
const formatToEngine: Record<string, string> = {
|
||||
openai: 'openai_compatible',
|
||||
anthropic: 'anthropic_compatible',
|
||||
ollama: 'ollama_compatible',
|
||||
};
|
||||
setEngine(formatToEngine[template.format] || 'openai_compatible');
|
||||
|
||||
const templateModels = template.models.filter((m) => !m.deprecated).map((m) => m.id);
|
||||
setModels(templateModels.join(', '));
|
||||
|
||||
setStep('form');
|
||||
};
|
||||
|
||||
const handleClearTemplate = () => {
|
||||
setSelectedTemplate(null);
|
||||
setDisplayName('');
|
||||
setApiUrl('');
|
||||
setModels('');
|
||||
setEngine('openai_compatible');
|
||||
setSupportsStreaming(true);
|
||||
setRequiresAuth(false);
|
||||
setStep('choice');
|
||||
};
|
||||
|
||||
const handleRequiresAuthChange = (checked: boolean) => {
|
||||
setRequiresAuth(checked);
|
||||
if (!checked) {
|
||||
setApiKey('');
|
||||
}
|
||||
@@ -81,28 +123,19 @@ export default function CustomProviderForm({
|
||||
const isDuplicate = headers.some((h) => h.key.trim().toLowerCase() === normalizedNewKey);
|
||||
|
||||
if (keyEmpty || valueEmpty) {
|
||||
setInvalidHeaderFields({
|
||||
key: keyEmpty,
|
||||
value: valueEmpty,
|
||||
});
|
||||
setInvalidHeaderFields({ key: keyEmpty, value: valueEmpty });
|
||||
setHeaderValidationError('Both header name and value must be entered');
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyHasSpaces) {
|
||||
setInvalidHeaderFields({
|
||||
key: true,
|
||||
value: false,
|
||||
});
|
||||
setInvalidHeaderFields({ key: true, value: false });
|
||||
setHeaderValidationError('Header name cannot contain spaces');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDuplicate) {
|
||||
setInvalidHeaderFields({
|
||||
key: true,
|
||||
value: false,
|
||||
});
|
||||
setInvalidHeaderFields({ key: true, value: false });
|
||||
setHeaderValidationError('A header with this name already exists');
|
||||
return;
|
||||
}
|
||||
@@ -120,16 +153,12 @@ export default function CustomProviderForm({
|
||||
|
||||
const handleHeaderChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||
if (field === 'key') {
|
||||
if (value.includes(' ')) {
|
||||
return;
|
||||
}
|
||||
if (value.includes(' ')) return;
|
||||
const normalizedValue = value.trim().toLowerCase();
|
||||
const isDuplicate = headers.some(
|
||||
(h, i) => i !== index && h.key.trim().toLowerCase() === normalizedValue
|
||||
);
|
||||
if (isDuplicate && normalizedValue !== '') {
|
||||
return;
|
||||
}
|
||||
if (isDuplicate && normalizedValue !== '') return;
|
||||
const updatedHeaders = [...headers];
|
||||
updatedHeaders[index].key = value;
|
||||
setHeaders(updatedHeaders);
|
||||
@@ -159,7 +188,7 @@ export default function CustomProviderForm({
|
||||
if (!displayName) errors.displayName = 'Display name is required';
|
||||
if (!apiUrl) errors.apiUrl = 'API URL is required';
|
||||
const existingHadAuth = initialData && (initialData.requires_auth ?? true);
|
||||
if (requiresApiKey && !apiKey && !existingHadAuth) errors.apiKey = 'API key is required';
|
||||
if (requiresAuth && !apiKey && !existingHadAuth) errors.apiKey = 'API key is required';
|
||||
if (!models) errors.models = 'At least one model is required';
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
@@ -201,102 +230,228 @@ export default function CustomProviderForm({
|
||||
api_key: apiKey,
|
||||
models: modelList,
|
||||
supports_streaming: supportsStreaming,
|
||||
requires_auth: requiresApiKey,
|
||||
requires_auth: requiresAuth,
|
||||
headers: headersObject,
|
||||
catalog_provider_id: selectedTemplate?.id ?? initialData?.catalog_provider_id ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
// Aggregate capability badges for template models
|
||||
const templateModelCapabilities = selectedTemplate?.models
|
||||
.filter((m) => !m.deprecated)
|
||||
.reduce(
|
||||
(acc, m) => {
|
||||
if (m.capabilities.tool_call) acc.tool_call = true;
|
||||
if (m.capabilities.reasoning) acc.reasoning = true;
|
||||
if (m.capabilities.attachment) acc.attachment = true;
|
||||
return acc;
|
||||
},
|
||||
{ tool_call: false, reasoning: false, attachment: false }
|
||||
);
|
||||
|
||||
// -- Step: Choice --
|
||||
if (step === 'choice') {
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<p className="text-sm text-textSubtle">Choose how you'd like to set up your provider.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep('catalog')}
|
||||
className="w-full p-4 text-left border border-border rounded-lg hover:bg-surfaceHover hover:border-primary transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Search className="w-5 h-5 text-primary flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium text-textStandard">Start from a provider template</div>
|
||||
<div className="text-sm text-textSubtle mt-0.5">
|
||||
Pick a known provider and we'll auto-fill the configuration
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep('form')}
|
||||
className="w-full p-4 text-left border border-border rounded-lg hover:bg-surfaceHover hover:border-primary transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="w-5 h-5 text-textSubtle flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium text-textStandard">Configure manually</div>
|
||||
<div className="text-sm text-textSubtle mt-0.5">
|
||||
Enter all provider details yourself
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -- Step: Catalog picker --
|
||||
if (step === 'catalog') {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<ProviderCatalogPicker onSelect={handleTemplateSelect} onCancel={onCancel} embedded />
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button type="button" variant="ghost" onClick={() => setStep('choice')}>
|
||||
← Back
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -- Step: Form --
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="mt-4 space-y-4">
|
||||
{isEditable && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="provider-select"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Provider Type
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="provider-select"
|
||||
aria-invalid={!!validationErrors.providerType}
|
||||
aria-describedby={validationErrors.providerType ? 'provider-select-error' : undefined}
|
||||
options={[
|
||||
{ value: 'openai_compatible', label: 'OpenAI Compatible' },
|
||||
{ value: 'anthropic_compatible', label: 'Anthropic Compatible' },
|
||||
{ value: 'ollama_compatible', label: 'Ollama Compatible' },
|
||||
]}
|
||||
value={{
|
||||
value: engine,
|
||||
label:
|
||||
engine === 'openai_compatible'
|
||||
? 'OpenAI Compatible'
|
||||
: engine === 'anthropic_compatible'
|
||||
? 'Anthropic Compatible'
|
||||
: 'Ollama Compatible',
|
||||
}}
|
||||
onChange={(option: unknown) => {
|
||||
const selectedOption = option as { value: string; label: string } | null;
|
||||
if (selectedOption) setEngine(selectedOption.value);
|
||||
}}
|
||||
isSearchable={false}
|
||||
/>
|
||||
{validationErrors.providerType && (
|
||||
<p id="provider-select-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.providerType}
|
||||
</p>
|
||||
)}
|
||||
{/* Template info banner */}
|
||||
{selectedTemplate && (
|
||||
<div className="p-3 bg-surfaceHover border border-border rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-textStandard">
|
||||
Using template: {selectedTemplate.name}
|
||||
</div>
|
||||
<div className="text-textSubtle mt-1">{selectedTemplate.api_url}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedTemplate.doc_url && (
|
||||
<a
|
||||
href={selectedTemplate.doc_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline text-sm flex items-center gap-1"
|
||||
>
|
||||
Docs <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClearTemplate}
|
||||
className="text-textSubtle hover:text-textStandard"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="display-name"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Display Name
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="display-name"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Your Provider Name"
|
||||
aria-invalid={!!validationErrors.displayName}
|
||||
aria-describedby={validationErrors.displayName ? 'display-name-error' : undefined}
|
||||
className={validationErrors.displayName ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.displayName && (
|
||||
<p id="display-name-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.displayName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="api-url"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
API URL
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="api-url"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/v1"
|
||||
aria-invalid={!!validationErrors.apiUrl}
|
||||
aria-describedby={validationErrors.apiUrl ? 'api-url-error' : undefined}
|
||||
className={validationErrors.apiUrl ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.apiUrl && (
|
||||
<p id="api-url-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.apiUrl}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Back to choice (create without template only) */}
|
||||
{!initialData && !selectedTemplate && (
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => setStep('choice')}>
|
||||
← Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Provider type dropdown */}
|
||||
{isEditable && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="provider-select"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Provider Type
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="provider-select"
|
||||
aria-invalid={!!validationErrors.providerType}
|
||||
aria-describedby={validationErrors.providerType ? 'provider-select-error' : undefined}
|
||||
options={[
|
||||
{ value: 'openai_compatible', label: 'OpenAI Compatible' },
|
||||
{ value: 'anthropic_compatible', label: 'Anthropic Compatible' },
|
||||
{ value: 'ollama_compatible', label: 'Ollama Compatible' },
|
||||
]}
|
||||
value={{
|
||||
value: engine,
|
||||
label:
|
||||
engine === 'openai_compatible'
|
||||
? 'OpenAI Compatible'
|
||||
: engine === 'anthropic_compatible'
|
||||
? 'Anthropic Compatible'
|
||||
: 'Ollama Compatible',
|
||||
}}
|
||||
onChange={(option: unknown) => {
|
||||
const selectedOption = option as { value: string; label: string } | null;
|
||||
if (selectedOption) setEngine(selectedOption.value);
|
||||
}}
|
||||
isSearchable={false}
|
||||
/>
|
||||
{validationErrors.providerType && (
|
||||
<p id="provider-select-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.providerType}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Display name */}
|
||||
{isEditable && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="display-name"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Display Name
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="display-name"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Your Provider Name"
|
||||
aria-invalid={!!validationErrors.displayName}
|
||||
aria-describedby={validationErrors.displayName ? 'display-name-error' : undefined}
|
||||
className={validationErrors.displayName ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.displayName && (
|
||||
<p id="display-name-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.displayName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API URL */}
|
||||
{isEditable && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="api-url"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
API URL
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="api-url"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/v1"
|
||||
aria-invalid={!!validationErrors.apiUrl}
|
||||
aria-describedby={validationErrors.apiUrl ? 'api-url-error' : undefined}
|
||||
className={validationErrors.apiUrl ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.apiUrl && (
|
||||
<p id="api-url-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.apiUrl}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Authentication */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">Authentication</label>
|
||||
<p className="text-sm text-text-secondary mb-3">
|
||||
@@ -305,23 +460,28 @@ export default function CustomProviderForm({
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="requires-api-key"
|
||||
checked={requiresApiKey}
|
||||
onChange={(e) => handleRequiresApiKeyChange(e.target.checked)}
|
||||
id="requires-auth"
|
||||
checked={requiresAuth}
|
||||
onChange={(e) => handleRequiresAuthChange(e.target.checked)}
|
||||
className="rounded border-border-primary"
|
||||
/>
|
||||
<label htmlFor="requires-api-key" className="text-sm text-text-secondary">
|
||||
<label htmlFor="requires-auth" className="text-sm text-text-secondary">
|
||||
This provider requires an API key
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{requiresApiKey && (
|
||||
{requiresAuth && (
|
||||
<div className="mt-3">
|
||||
<label
|
||||
htmlFor="api-key"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
API Key
|
||||
{selectedTemplate?.env_var && (
|
||||
<span className="text-textSubtle ml-1 font-normal">
|
||||
({selectedTemplate.env_var})
|
||||
</span>
|
||||
)}
|
||||
{!initialData && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<Input
|
||||
@@ -342,119 +502,145 @@ export default function CustomProviderForm({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Models */}
|
||||
{isEditable && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="available-models"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Available Models (comma-separated)
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="available-models"
|
||||
value={models}
|
||||
onChange={(e) => setModels(e.target.value)}
|
||||
placeholder="model-a, model-b, model-c"
|
||||
aria-invalid={!!validationErrors.models}
|
||||
aria-describedby={validationErrors.models ? 'available-models-error' : undefined}
|
||||
className={validationErrors.models ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.models && (
|
||||
<p id="available-models-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.models}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="supports-streaming"
|
||||
checked={supportsStreaming}
|
||||
onChange={(e) => setSupportsStreaming(e.target.checked)}
|
||||
className="rounded border-border-primary"
|
||||
/>
|
||||
<label htmlFor="supports-streaming" className="text-sm text-text-secondary">
|
||||
Provider supports streaming responses
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-textStandard mb-2 block">
|
||||
Custom Headers
|
||||
</label>
|
||||
<p className="text-xs text-textSubtle mb-4">
|
||||
Add custom HTTP headers to include in requests to the provider. Click the "+" button
|
||||
to add after filling both fields.
|
||||
<div>
|
||||
<label
|
||||
htmlFor="available-models"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Available Models (comma-separated)
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="available-models"
|
||||
value={models}
|
||||
onChange={(e) => setModels(e.target.value)}
|
||||
placeholder="model-a, model-b, model-c"
|
||||
aria-invalid={!!validationErrors.models}
|
||||
aria-describedby={validationErrors.models ? 'available-models-error' : undefined}
|
||||
className={validationErrors.models ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.models && (
|
||||
<p id="available-models-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.models}
|
||||
</p>
|
||||
<div className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
|
||||
{headers.map((header, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Input
|
||||
value={header.key}
|
||||
onChange={(e) => handleHeaderChange(index, 'key', e.target.value)}
|
||||
placeholder="Header name"
|
||||
className="w-full text-textStandard border-borderSubtle hover:border-borderStandard"
|
||||
/>
|
||||
<Input
|
||||
value={header.value}
|
||||
onChange={(e) => handleHeaderChange(index, 'value', e.target.value)}
|
||||
placeholder="Value"
|
||||
className="w-full text-textStandard border-borderSubtle hover:border-borderStandard"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handleRemoveHeader(index)}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="group p-2 h-auto text-iconSubtle hover:bg-transparent"
|
||||
>
|
||||
<X className="h-3 w-3 text-gray-400 group-hover:text-white group-hover:drop-shadow-sm transition-all" />
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
<Input
|
||||
value={newHeaderKey}
|
||||
onChange={(e) => {
|
||||
setNewHeaderKey(e.target.value);
|
||||
clearHeaderValidation();
|
||||
}}
|
||||
onKeyDown={handleHeaderKeyDown}
|
||||
placeholder="Header name"
|
||||
className={cn(
|
||||
'w-full text-textStandard border-borderSubtle hover:border-borderStandard',
|
||||
invalidHeaderFields.key && 'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
value={newHeaderValue}
|
||||
onChange={(e) => {
|
||||
setNewHeaderValue(e.target.value);
|
||||
clearHeaderValidation();
|
||||
}}
|
||||
onKeyDown={handleHeaderKeyDown}
|
||||
placeholder="Value"
|
||||
className={cn(
|
||||
'w-full text-textStandard border-borderSubtle hover:border-borderStandard',
|
||||
invalidHeaderFields.value && 'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleAddHeader}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-textStandard bg-background-primary border border-borderSubtle hover:border-borderStandard transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
|
||||
>
|
||||
<Plus /> Add
|
||||
</Button>
|
||||
)}
|
||||
{/* Capability badges when template is active */}
|
||||
{selectedTemplate && templateModelCapabilities && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
{templateModelCapabilities.tool_call && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300">
|
||||
Tool calling
|
||||
</span>
|
||||
)}
|
||||
{templateModelCapabilities.reasoning && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300">
|
||||
Reasoning
|
||||
</span>
|
||||
)}
|
||||
{templateModelCapabilities.attachment && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300">
|
||||
Attachments
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{headerValidationError && (
|
||||
<div className="mt-2 text-red-500 text-sm">{headerValidationError}</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Streaming */}
|
||||
{isEditable && (
|
||||
<div className="flex items-center space-x-2 mb-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="supports-streaming"
|
||||
checked={supportsStreaming}
|
||||
onChange={(e) => setSupportsStreaming(e.target.checked)}
|
||||
className="rounded border-border-primary"
|
||||
/>
|
||||
<label htmlFor="supports-streaming" className="text-sm text-text-secondary">
|
||||
Provider supports streaming responses
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom headers */}
|
||||
{isEditable && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-textStandard mb-2 block">Custom Headers</label>
|
||||
<p className="text-xs text-textSubtle mb-4">
|
||||
Add custom HTTP headers to include in requests to the provider. Click the "+" button to
|
||||
add after filling both fields.
|
||||
</p>
|
||||
<div className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
|
||||
{headers.map((header, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Input
|
||||
value={header.key}
|
||||
onChange={(e) => handleHeaderChange(index, 'key', e.target.value)}
|
||||
placeholder="Header name"
|
||||
className="w-full text-textStandard border-borderSubtle hover:border-borderStandard"
|
||||
/>
|
||||
<Input
|
||||
value={header.value}
|
||||
onChange={(e) => handleHeaderChange(index, 'value', e.target.value)}
|
||||
placeholder="Value"
|
||||
className="w-full text-textStandard border-borderSubtle hover:border-borderStandard"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handleRemoveHeader(index)}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="group p-2 h-auto text-iconSubtle hover:bg-transparent"
|
||||
>
|
||||
<X className="h-3 w-3 text-gray-400 group-hover:text-white group-hover:drop-shadow-sm transition-all" />
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
<Input
|
||||
value={newHeaderKey}
|
||||
onChange={(e) => {
|
||||
setNewHeaderKey(e.target.value);
|
||||
clearHeaderValidation();
|
||||
}}
|
||||
onKeyDown={handleHeaderKeyDown}
|
||||
placeholder="Header name"
|
||||
className={cn(
|
||||
'w-full text-textStandard border-borderSubtle hover:border-borderStandard',
|
||||
invalidHeaderFields.key && 'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
value={newHeaderValue}
|
||||
onChange={(e) => {
|
||||
setNewHeaderValue(e.target.value);
|
||||
clearHeaderValidation();
|
||||
}}
|
||||
onKeyDown={handleHeaderKeyDown}
|
||||
placeholder="Value"
|
||||
className={cn(
|
||||
'w-full text-textStandard border-borderSubtle hover:border-borderStandard',
|
||||
invalidHeaderFields.value && 'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleAddHeader}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-textStandard bg-background-primary border border-borderSubtle hover:border-borderStandard transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
|
||||
>
|
||||
<Plus /> Add
|
||||
</Button>
|
||||
</div>
|
||||
{headerValidationError && (
|
||||
<div className="mt-2 text-red-500 text-sm">{headerValidationError}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SecureStorageNotice />
|
||||
|
||||
{showDeleteConfirmation ? (
|
||||
|
||||
@@ -7,6 +7,7 @@ interface CardContainerProps {
|
||||
grayedOut: boolean;
|
||||
testId?: string;
|
||||
borderStyle?: 'solid' | 'dashed';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function GlowingRing() {
|
||||
@@ -35,6 +36,7 @@ export default function CardContainer({
|
||||
grayedOut = false,
|
||||
testId,
|
||||
borderStyle = 'solid',
|
||||
className = '',
|
||||
}: CardContainerProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -59,7 +61,8 @@ export default function CardContainer({
|
||||
grayedOut
|
||||
? 'border-border-primary'
|
||||
: 'border-border-primary hover:border-border-primary'
|
||||
}`}
|
||||
}
|
||||
${className}`}
|
||||
>
|
||||
{header && (
|
||||
<div style={{ opacity: grayedOut ? '0.5' : '1' }}>
|
||||
|
||||
@@ -70,7 +70,14 @@ export type AnalyticsEvent =
|
||||
| {
|
||||
name: 'onboarding_provider_selected';
|
||||
properties: {
|
||||
method: 'api_key' | 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'ollama' | 'local' | 'other';
|
||||
method:
|
||||
| 'api_key'
|
||||
| 'openrouter'
|
||||
| 'tetrate'
|
||||
| 'chatgpt_codex'
|
||||
| 'ollama'
|
||||
| 'local'
|
||||
| 'other';
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -80,7 +87,10 @@ export type AnalyticsEvent =
|
||||
| { name: 'onboarding_abandoned'; properties: { step: string; duration_seconds?: number } }
|
||||
| {
|
||||
name: 'onboarding_setup_failed';
|
||||
properties: { provider: 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'local'; error_message?: string };
|
||||
properties: {
|
||||
provider: 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'local';
|
||||
error_message?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
name: 'error_occurred';
|
||||
|
||||
Reference in New Issue
Block a user