From 68a96a5ad8039d636a4e73a36db4dd77bb1f9a26 Mon Sep 17 00:00:00 2001 From: Tanya Verma Date: Tue, 28 Apr 2026 01:20:18 -0700 Subject: [PATCH] feat: add Tinfoil as an inference provider --- bun.lock | 26 +++++++++++++++++++++++++- package.json | 1 + src/ai/fetch.ts | 25 +++++++++++++++++++++++++ src/db/tables.ts | 2 +- src/settings/models/detail.tsx | 2 +- src/settings/models/index.tsx | 33 +++++++++++++++++++++++++++------ src/settings/models/layout.tsx | 1 + src/settings/models/new.tsx | 3 ++- 8 files changed, 83 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index 126d49a88..d13b02466 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "thunderbolt", @@ -73,6 +72,7 @@ "remark-gfm": "^4.0.1", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", + "tinfoil": "^1.1.4", "uuid": "^13.0.0", "web-haptics": "^0.0.6", "zod": "^4.0.17", @@ -329,6 +329,12 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@freedomofpress/crypto-browser": ["@freedomofpress/crypto-browser@0.1.7", "", { "dependencies": { "@noble/curves": "^1.6.0" } }, "sha512-zjWmZDKdAu8g0Zq1IjBQ+sKQ/NpfzStBDFjy/qHUSMVEL4wNlNGtA7lhtw8v8asXa0yqF2QTYQ3rq6xCTQeADw=="], + + "@freedomofpress/sigstore-browser": ["@freedomofpress/sigstore-browser@0.1.13", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/tuf-browser": "^0.1.11", "@noble/curves": "^2.0.1" } }, "sha512-3YfmP9JQ5h8CAO/vKJEGYrFdzss6xWxYi5ZkbR4KoHlxIaLGrnu+5TQDoZVWhuhyfDBdLQqj6ypdN0W6PsH3Jw=="], + + "@freedomofpress/tuf-browser": ["@freedomofpress/tuf-browser@0.1.11", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7" } }, "sha512-d76ohB/AS5+zI+lnbiFMX/BIK3nT18hZ8q3Na6X4vyYaSYjXkeknzhAnbx1da+8ObWLwsaZLue46haEch28qtQ=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.8.9" } }, "sha512-DtZeRRHY9A/bisTJziUBBPrdnPui7+R185G/hzi6/Boymhqh7/wi53AY+IvQHS1+7OPaqfO/1XNpngNwthLz+A=="], "@hono/node-server": ["@hono/node-server@1.19.13", "", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="], @@ -415,6 +421,8 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@panva/hpke-noble": ["@panva/hpke-noble@1.1.0", "", { "dependencies": { "@noble/ciphers": "^2.0.1", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "@noble/post-quantum": "^0.6.0" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-1awnSeLKWEgCBGjuuFhKk6+AxxHzQGfeM0wP0LHtNQPWssXfTTdZvjXD5ruqguf+eNeLfTrMuC+89TNbEDGxTg=="], + "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -719,6 +727,8 @@ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@tinfoilsh/verifier": ["@tinfoilsh/verifier@1.1.4", "", { "dependencies": { "@freedomofpress/crypto-browser": "^0.1.7", "@freedomofpress/sigstore-browser": "^0.1.11", "@freedomofpress/tuf-browser": "^0.1.8" } }, "sha512-fcO0shcAIPBUG6xrWifsSF01motVa4fpma820FbgTtHzuaivoXzfeArylDXWlRcd+KWxn0wipVj0OCyjFMOr7w=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -1039,6 +1049,8 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "ehbp": ["ehbp@0.2.0", "", { "dependencies": { "@panva/hpke-noble": "^1.0.3", "hpke": "^1.0.1" } }, "sha512-hYkeupwvY0S1h9RpcrCD8pAgc6yfxfMqbI0y6khtS+ZNEByZAf116LTA6aBFB2JJQFsUaOEO7convZVrVhyeZA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.334", "", {}, "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1223,6 +1235,8 @@ "hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="], + "hpke": ["hpke@1.1.0", "", {}, "sha512-ta7pyqOF+ZIpDIRAH/4t+iE1vNrXfPHRLfYVCbuQKDOcYA6v/xBeyZStq2iglAEIafPkS0111G2wItUY2q/PiQ=="], + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], @@ -1597,6 +1611,8 @@ "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "openai": ["openai@6.34.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], @@ -1863,6 +1879,8 @@ "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "tinfoil": ["tinfoil@1.1.4", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.26", "@freedomofpress/sigstore-browser": "^0.1.11", "@tinfoilsh/verifier": "1.1.4", "ehbp": "^0.2.0", "openai": "^6.17.0" }, "peerDependencies": { "ai": "^6.0.69" }, "optionalPeers": ["ai"] }, "sha512-Ris0RaWb5uu+N02CX0LPhi4LwUiMoyEGUpxTF6CsTWWz6ThT9Z7P1VXoXwLwriuJnzePh/cwRA71UCULwoEmJg=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -2045,6 +2063,8 @@ "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@freedomofpress/crypto-browser/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -2061,6 +2081,8 @@ "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + "@panva/hpke-noble/@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + "@powersync/web/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -2249,6 +2271,8 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@freedomofpress/crypto-browser/@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], diff --git a/package.json b/package.json index 8b9186ba6..9ae246e24 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "remark-gfm": "^4.0.1", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", + "tinfoil": "^1.1.4", "uuid": "^13.0.0", "web-haptics": "^0.0.6", "zod": "^4.0.17", diff --git a/src/ai/fetch.ts b/src/ai/fetch.ts index 9a726cbd3..1d2172df2 100644 --- a/src/ai/fetch.ts +++ b/src/ai/fetch.ts @@ -19,6 +19,7 @@ import { createAnthropic } from '@ai-sdk/anthropic' import { createOpenAI } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import type { HttpClient } from '@/lib/http' +import { SecureClient } from 'tinfoil' import { v7 as uuidv7 } from 'uuid' // Currently @openrouter/ai-sdk-provider is NOT compatible with Vercel AI SDK v5. If you enable this, you will get the following error: @@ -49,6 +50,17 @@ export const ollama = createOpenAI({ fetch, }) +// Reuse one SecureClient across requests so attestation runs once per page load. +let tinfoilClient: SecureClient | null = null + +export const getTinfoilClient = async (): Promise => { + if (!tinfoilClient) { + tinfoilClient = new SecureClient() + } + await tinfoilClient.ready() + return tinfoilClient +} + type AiFetchStreamingResponseOptions = { init: RequestInit saveMessages: SaveMessagesFunction @@ -124,6 +136,19 @@ export const createModel = async (modelConfig: Model) => { }) return openrouter(modelConfig.model) } + case 'tinfoil': { + if (!modelConfig.apiKey) { + throw new Error('No API key provided') + } + const client = await getTinfoilClient() + const tinfoil = createOpenAICompatible({ + name: 'tinfoil', + baseURL: client.getBaseURL()!, + apiKey: modelConfig.apiKey, + fetch: client.fetch, + }) + return tinfoil(modelConfig.model) + } default: throw new Error(`Unsupported provider: ${modelConfig.provider}`) } diff --git a/src/db/tables.ts b/src/db/tables.ts index cc4e4b3d2..f8a4c53cd 100644 --- a/src/db/tables.ts +++ b/src/db/tables.ts @@ -78,7 +78,7 @@ export const modelsTable = sqliteTable( { id: text('id').primaryKey(), provider: text('provider', { - enum: ['openai', 'custom', 'openrouter', 'thunderbolt', 'anthropic'], + enum: ['openai', 'custom', 'openrouter', 'thunderbolt', 'anthropic', 'tinfoil'], }), name: text('name'), model: text('model'), diff --git a/src/settings/models/detail.tsx b/src/settings/models/detail.tsx index ff355171a..5d9de2d67 100644 --- a/src/settings/models/detail.tsx +++ b/src/settings/models/detail.tsx @@ -28,7 +28,7 @@ import { Trash2 } from 'lucide-react' const formSchema = z .object({ - provider: z.enum(['thunderbolt', 'anthropic', 'openai', 'custom', 'openrouter']), + provider: z.enum(['thunderbolt', 'anthropic', 'openai', 'custom', 'openrouter', 'tinfoil']), name: z.string().min(1, { message: 'Name is required.' }), model: z.string().min(1, { message: 'Model name is required.' }), url: z.string().optional(), diff --git a/src/settings/models/index.tsx b/src/settings/models/index.tsx index 03569e73f..4cad57852 100644 --- a/src/settings/models/index.tsx +++ b/src/settings/models/index.tsx @@ -1,4 +1,4 @@ -import { createModel } from '@/ai/fetch' +import { createModel, getTinfoilClient } from '@/ai/fetch' import { ModificationIndicator } from '@/components/modification-indicator' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -147,7 +147,7 @@ const modelReducer = (state: ModelState, action: ModelAction): ModelState => { const formSchema = z .object({ - provider: z.enum(['thunderbolt', 'anthropic', 'openai', 'custom', 'openrouter']), + provider: z.enum(['thunderbolt', 'anthropic', 'openai', 'custom', 'openrouter', 'tinfoil']), name: z.string().min(1, { message: 'Name is required.' }), model: z.string().min(1, { message: 'Model name is required.' }), customModel: z.string().optional(), @@ -403,6 +403,24 @@ export default function ModelsPage() { endpoint = 'https://openrouter.ai/api/v1/models' headers = { Authorization: `Bearer ${apiKey}` } break + case 'tinfoil': { + // /v1/models is unauthenticated, but route through SecureClient so + // attestation is warmed up before the user's first chat. + const client = await getTinfoilClient() + const response = await http.get(`${client.getBaseURL()}models`, { fetch: client.fetch }).json<{ + data: Array + }>() + + // The catalog also includes embedding, audio, document, and tts + // models; filter to ones that expose chat completions. + const tinfoilModels = (response.data || []) + .filter((m) => Array.isArray(m.endpoints) && m.endpoints.includes('/v1/chat/completions')) + .map((m) => ({ ...m, supports_tools: m.tool_calling === true })) + .sort((a, b) => a.id.localeCompare(b.id)) + + dispatch({ type: 'FETCH_MODELS_SUCCESS', models: tinfoilModels }) + return + } case 'thunderbolt': { const thunderboltModels = [ { id: 'kimi-k2-instruct', name: 'Kimi K2', supports_tools: true }, @@ -603,7 +621,7 @@ export default function ModelsPage() { form.setValue('toolUsage', true, { shouldValidate: false, shouldDirty: false }) // Fetch models if we have the necessary credentials - if (['thunderbolt', 'anthropic'].includes(currentProvider)) { + if (['thunderbolt', 'tinfoil', 'anthropic'].includes(currentProvider)) { fetchAvailableModels(currentProvider) } @@ -633,6 +651,8 @@ export default function ModelsPage() { switch (provider) { case 'thunderbolt': return 'Thunderbolt' + case 'tinfoil': + return 'Tinfoil' case 'anthropic': return 'Anthropic' case 'openai': @@ -678,7 +698,7 @@ export default function ModelsPage() { const watchedModel = form.watch('model') const canTestConnection = useMemo(() => { - if (watchedProvider === 'anthropic') { + if (['anthropic', 'tinfoil'].includes(watchedProvider)) { return !!watchedModel && watchedApiKey } @@ -714,6 +734,7 @@ export default function ModelsPage() { Thunderbolt + Tinfoil OpenAI OpenRouter Anthropic @@ -778,13 +799,13 @@ export default function ModelsPage() { const url = form.watch('url') // Show model selection if: - // 1. Thunderbolt (no API key needed) + // 1. Thunderbolt / Tinfoil (no API key needed) // 1. Anthropic (API key required for testing - model list is hardwired) // 2. Other providers with API key // 3. OpenAI Compatible with URL (API key optional) const showModelSelection = !modelLoadError && - (['thunderbolt', 'anthropic'].includes(provider) || + (['thunderbolt', 'tinfoil', 'anthropic'].includes(provider) || (provider && apiKey) || (provider === 'custom' && url)) diff --git a/src/settings/models/layout.tsx b/src/settings/models/layout.tsx index 3d8fa4f1e..684ce2ded 100644 --- a/src/settings/models/layout.tsx +++ b/src/settings/models/layout.tsx @@ -57,6 +57,7 @@ export default function ModelsLayout() {

{model.provider === 'thunderbolt' && 'Thunderbolt'} + {model.provider === 'tinfoil' && 'Tinfoil'} {model.provider === 'openai' && 'OpenAI'} {model.provider === 'openrouter' && 'OpenRouter'} {model.provider === 'custom' && 'Custom'} - {model.model} diff --git a/src/settings/models/new.tsx b/src/settings/models/new.tsx index cc2f46067..2fa343944 100644 --- a/src/settings/models/new.tsx +++ b/src/settings/models/new.tsx @@ -16,7 +16,7 @@ import type { Model } from '@/types' const formSchema = z .object({ - provider: z.enum(['thunderbolt', 'openai', 'custom', 'openrouter']), + provider: z.enum(['thunderbolt', 'openai', 'custom', 'openrouter', 'tinfoil']), name: z.string().min(1, { message: 'Name is required.' }), model: z.string().min(1, { message: 'Model name is required.' }), url: z.string().optional(), @@ -117,6 +117,7 @@ export default function NewModelPage() { Thunderbolt + Tinfoil OpenAI OpenRouter Custom