Merge 571694e39d into 09be5d45d5
This commit is contained in:
commit
a835f354c3
76
docs/providers/firmware.md
Normal file
76
docs/providers/firmware.md
Normal file
@ -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)
|
||||||
175
src/agents/firmware-models.ts
Normal file
175
src/agents/firmware-models.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -286,6 +286,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
venice: "VENICE_API_KEY",
|
venice: "VENICE_API_KEY",
|
||||||
mistral: "MISTRAL_API_KEY",
|
mistral: "MISTRAL_API_KEY",
|
||||||
opencode: "OPENCODE_API_KEY",
|
opencode: "OPENCODE_API_KEY",
|
||||||
|
firmware: "FIRMWARE_API_KEY",
|
||||||
};
|
};
|
||||||
const envVar = envMap[normalized];
|
const envVar = envMap[normalized];
|
||||||
if (!envVar) return null;
|
if (!envVar) return null;
|
||||||
|
|||||||
40
src/agents/models-config.providers.firmware.test.ts
Normal file
40
src/agents/models-config.providers.firmware.test.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it, beforeEach, afterEach } 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", () => {
|
||||||
|
let previousFirmwareKey: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
previousFirmwareKey = process.env.FIRMWARE_API_KEY;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (previousFirmwareKey !== undefined) {
|
||||||
|
process.env.FIRMWARE_API_KEY = previousFirmwareKey;
|
||||||
|
} else {
|
||||||
|
delete process.env.FIRMWARE_API_KEY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not include firmware when no API key is configured", async () => {
|
||||||
|
delete process.env.FIRMWARE_API_KEY;
|
||||||
|
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 () => {
|
||||||
|
process.env.FIRMWARE_API_KEY = "test-firmware-key";
|
||||||
|
const agentDir = mkdtempSync(join(tmpdir(), "clawd-test-"));
|
||||||
|
const providers = await resolveImplicitProviders({ agentDir });
|
||||||
|
|
||||||
|
expect(providers?.firmware).toBeDefined();
|
||||||
|
expect(providers?.firmware?.baseUrl).toBe("https://app.firmware.ai/api/v1");
|
||||||
|
expect(providers?.firmware?.api).toBe("openai-completions");
|
||||||
|
expect(providers?.firmware?.models?.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -13,6 +13,11 @@ import {
|
|||||||
SYNTHETIC_MODEL_CATALOG,
|
SYNTHETIC_MODEL_CATALOG,
|
||||||
} from "./synthetic-models.js";
|
} from "./synthetic-models.js";
|
||||||
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-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<OpenClawConfig["models"]>;
|
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
|
||||||
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[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 {
|
function buildSyntheticProvider(): ProviderConfig {
|
||||||
return {
|
return {
|
||||||
baseUrl: SYNTHETIC_BASE_URL,
|
baseUrl: SYNTHETIC_BASE_URL,
|
||||||
@ -446,6 +459,13 @@ export async function resolveImplicitProviders(params: {
|
|||||||
providers.xiaomi = { ...buildXiaomiProvider(), apiKey: xiaomiKey };
|
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
|
// Ollama provider - only add if explicitly configured
|
||||||
const ollamaKey =
|
const ollamaKey =
|
||||||
resolveEnvApiKeyVarName("ollama") ??
|
resolveEnvApiKeyVarName("ollama") ??
|
||||||
|
|||||||
@ -54,6 +54,7 @@ describe("models-config", () => {
|
|||||||
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
||||||
const previousVenice = process.env.VENICE_API_KEY;
|
const previousVenice = process.env.VENICE_API_KEY;
|
||||||
const previousXiaomi = process.env.XIAOMI_API_KEY;
|
const previousXiaomi = process.env.XIAOMI_API_KEY;
|
||||||
|
const previousFirmware = process.env.FIRMWARE_API_KEY;
|
||||||
delete process.env.COPILOT_GITHUB_TOKEN;
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
delete process.env.GH_TOKEN;
|
delete process.env.GH_TOKEN;
|
||||||
delete process.env.GITHUB_TOKEN;
|
delete process.env.GITHUB_TOKEN;
|
||||||
@ -63,6 +64,7 @@ describe("models-config", () => {
|
|||||||
delete process.env.SYNTHETIC_API_KEY;
|
delete process.env.SYNTHETIC_API_KEY;
|
||||||
delete process.env.VENICE_API_KEY;
|
delete process.env.VENICE_API_KEY;
|
||||||
delete process.env.XIAOMI_API_KEY;
|
delete process.env.XIAOMI_API_KEY;
|
||||||
|
delete process.env.FIRMWARE_API_KEY;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@ -97,6 +99,8 @@ describe("models-config", () => {
|
|||||||
else process.env.VENICE_API_KEY = previousVenice;
|
else process.env.VENICE_API_KEY = previousVenice;
|
||||||
if (previousXiaomi === undefined) delete process.env.XIAOMI_API_KEY;
|
if (previousXiaomi === undefined) delete process.env.XIAOMI_API_KEY;
|
||||||
else process.env.XIAOMI_API_KEY = previousXiaomi;
|
else process.env.XIAOMI_API_KEY = previousXiaomi;
|
||||||
|
if (previousFirmware === undefined) delete process.env.FIRMWARE_API_KEY;
|
||||||
|
else process.env.FIRMWARE_API_KEY = previousFirmware;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user