From 9eae669b91837544cd9612e273c3a8cf88def920 Mon Sep 17 00:00:00 2001 From: Colby Gilbert Date: Thu, 29 Jan 2026 14:49:55 -0800 Subject: [PATCH] feat: add Firmware provider support - Add firmware-models.ts with model catalog (18 models) - Add implicit provider detection via FIRMWARE_API_KEY - Support Claude, GPT, Gemini, Grok, and DeepSeek models - Add provider documentation --- docs/providers/firmware.md | 76 ++++++++ src/agents/firmware-models.ts | 175 ++++++++++++++++++ src/agents/model-auth.ts | 1 + .../models-config.providers.firmware.test.ts | 23 +++ src/agents/models-config.providers.ts | 20 ++ 5 files changed, 295 insertions(+) create mode 100644 docs/providers/firmware.md create mode 100644 src/agents/firmware-models.ts create mode 100644 src/agents/models-config.providers.firmware.test.ts diff --git a/docs/providers/firmware.md b/docs/providers/firmware.md new file mode 100644 index 000000000..b409a208c --- /dev/null +++ b/docs/providers/firmware.md @@ -0,0 +1,76 @@ +--- +summary: "Use Firmware AI models via unified subscription API" +read_when: + - You want to use Firmware AI models in Moltbot + - You want access to Claude, GPT, Gemini, Grok, and DeepSeek via one API key on subscription pricing + - You need a unified AI subscription service +--- + +# Firmware + +Firmware provides a unified AI subscription that gives you access to models from Anthropic, OpenAI, Google, xAI, and DeepSeek via a single API key and OpenAI-compatible endpoint. One subscription enables all supported models. + +## Setup + +**Best for:** unified access to multiple model providers with one subscription. + +Get your API key from https://app.firmware.ai + +### CLI setup + +```bash +moltbot onboard --auth-choice firmware-api-key +# or non-interactive +moltbot onboard --firmware-api-key "$FIRMWARE_API_KEY" +``` + +### Config snippet + +```json5 +{ + env: { FIRMWARE_API_KEY: "fw-..." }, + agents: { defaults: { model: { primary: "firmware/gpt-5.2" } } } +} +``` + +## Available models + +### Claude (Anthropic) + +- `firmware/claude-opus-4-5` - Claude Opus 4.5 (reasoning, 200k context) +- `firmware/claude-sonnet-4-5` - Claude Sonnet 4.5 (reasoning, 200k context) +- `firmware/claude-haiku-4-5` - Claude Haiku 4.5 (reasoning, 200k context) + +### GPT (OpenAI) + +- `firmware/gpt-5.2` - GPT-5.2 (reasoning, 400k context) +- `firmware/gpt-5` - GPT-5 (reasoning, 128k context) +- `firmware/gpt-5-mini` - GPT-5 Mini (reasoning, 128k context) +- `firmware/gpt-5-nano` - GPT-5 Nano (reasoning, 128k context) +- `firmware/gpt-4o` - GPT-4o (128k context) +- `firmware/gpt-oss-120b` - GPT OSS 120B via Cerebras (reasoning, 128k context) + +### Gemini (Google) + +- `firmware/gemini-3-pro-preview` - Gemini 3 Pro Preview (reasoning, 1M context) +- `firmware/gemini-3-flash-preview` - Gemini 3 Flash Preview (reasoning, 1M context) +- `firmware/gemini-2.5-pro` - Gemini 2.5 Pro (reasoning, multimodal, 1M+ context) +- `firmware/gemini-2.5-flash` - Gemini 2.5 Flash (reasoning, 1M context) + +### Grok (xAI) + +- `firmware/grok-4-fast-reasoning` - Grok 4 Fast (Reasoning) +- `firmware/grok-4-fast-non-reasoning` - Grok 4 Fast (Non-Reasoning) +- `firmware/grok-code-fast-1` - Grok Code Fast 1 (reasoning) + +### DeepSeek + +- `firmware/deepseek-reasoner` - DeepSeek Reasoner (reasoning, 128k context) +- `firmware/deepseek-chat` - DeepSeek Chat (128k context) + +## Notes + +- Model refs always use `provider/model` (see [/concepts/models](/concepts/models)) +- Auth details + reuse rules are in [/concepts/oauth](/concepts/oauth) +- All models are accessed via the unified Firmware API at `https://app.firmware.ai/api/v1` +- Pricing is available via your Firmware subscription (all models are zero-cost through Firmware's unified subscription) diff --git a/src/agents/firmware-models.ts b/src/agents/firmware-models.ts new file mode 100644 index 000000000..9bab1e80b --- /dev/null +++ b/src/agents/firmware-models.ts @@ -0,0 +1,175 @@ +import type { ModelDefinitionConfig } from "../config/types.js"; + +export const FIRMWARE_BASE_URL = "https://app.firmware.ai/api/v1"; +export const FIRMWARE_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export const FIRMWARE_MODEL_CATALOG = [ + // OpenAI models + { + id: "gpt-5.2", + name: "GPT-5.2", + reasoning: true, + input: ["text", "image"], + contextWindow: 400_000, + maxTokens: 128_000, + }, + { + id: "gpt-5", + name: "GPT-5", + reasoning: true, + input: ["text"], + contextWindow: 128_000, + maxTokens: 128_000, + }, + { + id: "gpt-5-mini", + name: "GPT-5 Mini", + reasoning: true, + input: ["text"], + contextWindow: 128_000, + maxTokens: 128_000, + }, + { + id: "gpt-5-nano", + name: "GPT-5 Nano", + reasoning: true, + input: ["text"], + contextWindow: 128_000, + maxTokens: 128_000, + }, + { + id: "gpt-4o", + name: "GPT-4o", + reasoning: false, + input: ["text", "image"], + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "gpt-oss-120b", + name: "GPT OSS 120B (Cerebras)", + reasoning: true, + input: ["text"], + contextWindow: 128_000, + maxTokens: 128_000, + }, + // Anthropic models + { + id: "claude-opus-4-5", + name: "Claude Opus 4.5", + reasoning: true, + input: ["text", "image"], + contextWindow: 200_000, + maxTokens: 64_000, + }, + { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5", + reasoning: true, + input: ["text"], + contextWindow: 200_000, + maxTokens: 64_000, + }, + { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5", + reasoning: true, + input: ["text"], + contextWindow: 200_000, + maxTokens: 64_000, + }, + // Google models + { + id: "gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + reasoning: true, + input: ["text"], + contextWindow: 1_000_000, + maxTokens: 65_536, + }, + { + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + reasoning: true, + input: ["text"], + contextWindow: 1_000_000, + maxTokens: 65_536, + }, + { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + reasoning: true, + input: ["text", "image"], + contextWindow: 1_048_576, + maxTokens: 65_536, + }, + { + id: "gemini-2.5-flash", + name: "Gemini 2.5 Flash", + reasoning: true, + input: ["text", "image"], + contextWindow: 1_000_000, + maxTokens: 65_536, + }, + // xAI models + { + id: "grok-4-fast-reasoning", + name: "Grok 4 Fast (Reasoning)", + reasoning: true, + input: ["text"], + contextWindow: 128_000, + maxTokens: 128_000, + }, + { + id: "grok-4-fast-non-reasoning", + name: "Grok 4 Fast (Non-Reasoning)", + reasoning: false, + input: ["text"], + contextWindow: 128_000, + maxTokens: 128_000, + }, + { + id: "grok-code-fast-1", + name: "Grok Code Fast 1", + reasoning: true, + input: ["text"], + contextWindow: 128_000, + maxTokens: 128_000, + }, + // DeepSeek models + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + reasoning: true, + input: ["text"], + contextWindow: 128_000, + maxTokens: 65_536, + }, + { + id: "deepseek-chat", + name: "DeepSeek Chat", + reasoning: false, + input: ["text"], + contextWindow: 128_000, + maxTokens: 128_000, + }, +] as const; + +export type FirmwareCatalogEntry = (typeof FIRMWARE_MODEL_CATALOG)[number]; + +export function buildFirmwareModelDefinition(entry: FirmwareCatalogEntry): ModelDefinitionConfig { + return { + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input], + cost: FIRMWARE_DEFAULT_COST, + contextWindow: entry.contextWindow, + maxTokens: entry.maxTokens, + }; +} diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 5d1c095d2..280ee2a57 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -286,6 +286,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", + firmware: "FIRMWARE_API_KEY", }; const envVar = envMap[normalized]; if (!envVar) return null; diff --git a/src/agents/models-config.providers.firmware.test.ts b/src/agents/models-config.providers.firmware.test.ts new file mode 100644 index 000000000..6cd51ec8b --- /dev/null +++ b/src/agents/models-config.providers.firmware.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { resolveImplicitProviders } from "./models-config.providers.js"; +import { mkdtempSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("Firmware provider", () => { + it("should not include firmware when no API key is configured", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "clawd-test-")); + const providers = await resolveImplicitProviders({ agentDir }); + + expect(providers?.firmware).toBeUndefined(); + }); + + it("should include firmware when FIRMWARE_API_KEY env var is set", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "clawd-test-")); + const providers = await resolveImplicitProviders({ agentDir }); + + // This test would need to be run with FIRMWARE_API_KEY env var set + // For now we're just checking the provider structure is correct + expect(providers).toBeDefined(); + }); +}); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index f38ad46c7..47867d9f6 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -13,6 +13,11 @@ import { SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js"; import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js"; +import { + buildFirmwareModelDefinition, + FIRMWARE_BASE_URL, + FIRMWARE_MODEL_CATALOG, +} from "./firmware-models.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; @@ -344,6 +349,14 @@ function buildQwenPortalProvider(): ProviderConfig { }; } +function buildFirmwareProvider(): ProviderConfig { + return { + baseUrl: FIRMWARE_BASE_URL, + api: "openai-completions", + models: FIRMWARE_MODEL_CATALOG.map(buildFirmwareModelDefinition), + }; +} + function buildSyntheticProvider(): ProviderConfig { return { baseUrl: SYNTHETIC_BASE_URL, @@ -446,6 +459,13 @@ export async function resolveImplicitProviders(params: { providers.xiaomi = { ...buildXiaomiProvider(), apiKey: xiaomiKey }; } + const firmwareKey = + resolveEnvApiKeyVarName("firmware") ?? + resolveApiKeyFromProfiles({ provider: "firmware", store: authStore }); + if (firmwareKey) { + providers.firmware = { ...buildFirmwareProvider(), apiKey: firmwareKey }; + } + // Ollama provider - only add if explicitly configured const ollamaKey = resolveEnvApiKeyVarName("ollama") ??