feat(provider): add Fireworks AI as official OpenAI-compatible provider (#1590)

* feat(provider): add Fireworks AI as official OpenAI-compatible provider

Includes vendor descriptor, brand descriptor (276 models), model
descriptors (full + merged), routing metadata, env auto-detection,
profile support, client defaults, and docs.

* test: add focused regression tests for Fireworks AI auth and routing

- Add 7 env-only routing tests in client.test.ts (shim routing,
  stale model replacement, base URL override, shim option cleanup,
  non-Fireworks override ignored, priority with MiniMax, Bedrock yield)
- Add FIREWORKS_API_KEY auto-detection test in providerAutoDetect.test.ts
- Add profile apply/persistence/env-drift tests in providerProfiles.test.ts
- Fix FIREWORKS_API_KEY propagation in strictEnv early return path

* fix: address reviewer comments on Fireworks integration

- Remove OPENAI_API_KEY exclusion so Fireworks cred wins over stale OpenAI key
- Fix TS type error in test by using String() wrapper
- Add Fireworks to detection priority comment in providerAutoDetect.ts
- Add useFireworksEnvOnlyProvider to shim condition for pattern consistency
- Replace loose .includes('fireworks.ai') with isFireworksBaseUrl() exact hostname check

* fix: add explicit case 'fireworks' in applyProviderFlag for credential precedence

- Add 'fireworks' to PREFERRED_PROVIDER_ORDER
- Add case 'fireworks' with dedicated key winning pattern (mirrors atlas-cloud)
- Add FIREWORKS_API_KEY to copiedOpenAIKeyProvider detection so stale
  keys are cleaned up when switching away from Fireworks

* fix: guard fireworks defaultModel assignment against 'undefined' string coercion

* fix: remove leftover conflict marker in providerProfiles.ts

* docs(fireworks): add JSDoc to Fireworks functions for coderabbit docstring coverage

Adds JSDoc annotations to isFireworksBaseUrl, getFireworksBaseUrlOverride,
hasFireworksEnvOnlyProviderIntent, isFireworksModelName, and
applyFireworksEnvOnlyDefaults.

* fix(fireworks): cross-check NEARAI_API_KEY in env-only intent functions

hasNearaiEnvOnlyProviderIntent and hasFireworksEnvOnlyProviderIntent were
missing mutual cross-checks. When both NEARAI_API_KEY and FIREWORKS_API_KEY
are set, neither excludes the other, and nearai silently wins by ordering.
Adding !hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) to the nearai intent
and !hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) to the fireworks intent
ensures both return false, forcing explicit provider selection.

* fix(fireworks): fix typo in JSDoc — OPENAI_API_API_BASE -> OPENAI_API_BASE

* fix(fireworks): remove merge artifact and preserve no-key auth headers

- src/utils/providerAutoDetect.ts: remove leftover ======= conflict
  marker and stale duplicate priority lines
- src/utils/providerProfiles.ts: preserve apiFormat, authHeader,
  authScheme, authHeaderValue in the no-key OpenAI-compatible
  fallback path so saved Responses mode / custom auth config
  survives restart

* fix: Fireworks env-only startup preservation and MIMO priority comment

- Add FIREWORKS_API_KEY check to hasConcreteProviderSelection() so env-only
  Fireworks setup is not overwritten by Gitlawb Opengateway default
- Add regression test verifying FIREWORKS_API_KEY survives no-profile startup
- Fix providerAutoDetect.ts priority comment to include MIMO_API_KEY (position 8)
  and renumber subsequent entries to match actual detection order

* fix: also preserve env-only NEAR AI startup in hasConcreteProviderSelection()

* fix: remove duplicate Fireworks model descriptor, add FIREWORKS_API_KEY to test env cleanup

* fix: move duplicate model check to generation-time, add OPENAI_AUTH_* env cleanup to test harness

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
This commit is contained in:
Gravirei
2026-06-13 01:10:19 +08:00
committed by GitHub
co-authored by Gravirei
parent d08593de92
commit 0b24b60ce9
20 changed files with 7145 additions and 31 deletions
+13
View File
@@ -306,6 +306,19 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# OPENAI_BASE_URL=https://qwen35-122b.completions.near.ai/v1
# -----------------------------------------------------------------------------
# Option 12: Fireworks AI
# -----------------------------------------------------------------------------
# Fireworks AI provides a fully OpenAI-compatible endpoint.
# Get your API key from https://fireworks.ai/
# Model IDs use the full path format: accounts/fireworks/models/<model-name>
#
# CLAUDE_CODE_USE_OPENAI=1
# FIREWORKS_API_KEY=fw_your_key_here
# OPENAI_BASE_URL=https://api.fireworks.ai/inference/v1
# OPENAI_MODEL=accounts/fireworks/models/llama-v3p1-70b-instruct
# =============================================================================
# OPTIONAL TUNING
# =============================================================================
+1
View File
@@ -163,6 +163,7 @@ Advanced and source-build guides:
| --- | --- | --- |
| OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers |
| Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models |
| Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` |
| Gemini | `/provider` or env vars | Supports API key only |
| GitHub Models | `/onboard-github` | Interactive onboarding with saved credentials |
| Codex OAuth | `/provider` | Opens ChatGPT sign-in in your browser and stores Codex credentials securely |
+11
View File
@@ -303,6 +303,17 @@ If your hostname is not detected as Azure (for example some inference endpoints)
export OPENAI_AZURE_STYLE=1
```
### Fireworks AI
Fireworks AI provides a fully OpenAI-compatible endpoint. Model IDs use the full path format `accounts/fireworks/models/<model-name>`.
```bash
export CLAUDE_CODE_USE_OPENAI=1
export FIREWORKS_API_KEY=fw_your_key_here
export OPENAI_BASE_URL=https://api.fireworks.ai/inference/v1
export OPENAI_MODEL=accounts/fireworks/models/llama-v3p1-70b-instruct
```
The **OpenClaude VS Code extension** can store the key in Secret Storage and set these variables for you when you launch from the Control Center. See `vscode-extension/openclaude-vscode/README.md`.
## Environment Variables
+1
View File
@@ -119,6 +119,7 @@ const PRESET_ORDER = [
'DeepSeek',
'Codex OAuth',
'xAI OAuth (Grok)',
'Fireworks AI',
'Google Gemini',
'Groq',
'Hicap',
+8
View File
@@ -394,6 +394,14 @@ function renderIntegrationArtifacts(
...modelModules.map(module => `import ${module.importName} from '${module.importPath}'`),
]
const allModelDescriptors = loadedModules.modelModules.flatMap(m => m.descriptors)
const uniqueKeys = new Set(allModelDescriptors.map(m => `${m.id}::${m.vendorId}`))
if (allModelDescriptors.length > uniqueKeys.size) {
throw new Error(
`Duplicate model (id, vendorId) pairs detected: ${allModelDescriptors.length} entries, ${uniqueKeys.size} unique.`,
)
}
const presetManifest = loadedModules.routeModules
.filter(routeModule => routeModule.descriptor.preset)
.map(routeModule => buildPresetManifestEntry(routeModule, routeModule.descriptor.preset!))
+292
View File
@@ -0,0 +1,292 @@
import { defineBrand } from '../define.js'
export default defineBrand({
id: 'fireworks',
label: 'Fireworks AI',
canonicalVendorId: 'fireworks',
defaultCapabilities: {
supportsVision: true,
supportsStreaming: true,
supportsFunctionCalling: true,
supportsJsonMode: true,
supportsPreciseTokenCount: false,
},
modelIds: [
'accounts/fireworks/models/chronos-hermes-13b-v2',
'accounts/fireworks/models/codegemma-2b',
'accounts/fireworks/models/codegemma-7b',
'accounts/fireworks/models/code-llama-13b',
'accounts/fireworks/models/code-llama-13b-instruct',
'accounts/fireworks/models/code-llama-13b-python',
'accounts/fireworks/models/code-llama-34b',
'accounts/fireworks/models/code-llama-34b-instruct',
'accounts/fireworks/models/code-llama-34b-python',
'accounts/fireworks/models/code-llama-70b',
'accounts/fireworks/models/code-llama-70b-instruct',
'accounts/fireworks/models/code-llama-70b-python',
'accounts/fireworks/models/code-llama-7b',
'accounts/fireworks/models/code-llama-7b-instruct',
'accounts/fireworks/models/code-llama-7b-python',
'accounts/fireworks/models/code-qwen-1p5-7b',
'accounts/fireworks/models/cogito-v1-preview-llama-3b',
'accounts/fireworks/models/cogito-v1-preview-llama-70b',
'accounts/fireworks/models/cogito-v1-preview-llama-8b',
'accounts/fireworks/models/cogito-v1-preview-qwen-14b',
'accounts/fireworks/models/cogito-v1-preview-qwen-32b',
'accounts/fireworks/models/dbrx-instruct',
'accounts/fireworks/models/deepseek-coder-1b-base',
'accounts/fireworks/models/deepseek-coder-33b-instruct',
'accounts/fireworks/models/deepseek-coder-7b-base',
'accounts/fireworks/models/deepseek-coder-7b-base-v1p5',
'accounts/fireworks/models/deepseek-coder-7b-instruct-v1p5',
'accounts/fireworks/models/deepseek-coder-v2-instruct',
'accounts/fireworks/models/deepseek-coder-v2-lite-base',
'accounts/fireworks/models/deepseek-coder-v2-lite-instruct',
'accounts/fireworks/models/deepseek-prover-v2',
'accounts/fireworks/models/deepseek-r1',
'accounts/fireworks/models/deepseek-r1-0528',
'accounts/fireworks/models/deepseek-r1-0528-distill-qwen3-8b',
'accounts/fireworks/models/deepseek-r1-basic',
'accounts/fireworks/models/deepseek-r1-distill-llama-70b',
'accounts/fireworks/models/deepseek-r1-distill-llama-8b',
'accounts/fireworks/models/deepseek-r1-distill-qwen-14b',
'accounts/fireworks/models/deepseek-r1-distill-qwen-1p5b',
'accounts/fireworks/models/deepseek-r1-distill-qwen-32b',
'accounts/fireworks/models/deepseek-r1-distill-qwen-7b',
'accounts/fireworks/models/deepseek-v2-lite-chat',
'accounts/fireworks/models/deepseek-v2p5',
'accounts/fireworks/models/deepseek-v3',
'accounts/fireworks/models/deepseek-v3-0324',
'accounts/fireworks/models/deepseek-v3p1',
'accounts/fireworks/models/deepseek-v3p2',
'accounts/fireworks/models/deepseek-v4-flash',
'accounts/fireworks/models/deepseek-v4-pro',
'accounts/fireworks/models/devstral-small-2-24b-instruct-2512',
'accounts/fireworks/models/devstral-small-2505',
'accounts/fireworks/models/dolphin-2-9-2-qwen2-72b',
'accounts/fireworks/models/dolphin-2p6-mixtral-8x7b',
'accounts/fireworks/models/ernie-4p5-21b-a3b-pt',
'accounts/fireworks/models/ernie-4p5-300b-a47b-pt',
'accounts/fireworks/models/fare-20b',
'accounts/fireworks/models/firefunction-v1',
'accounts/fireworks/models/firefunction-v2',
'accounts/fireworks/models/firellava-13b',
'accounts/fireworks/models/firesearch-ocr-v6',
'accounts/fireworks/models/gemma-2b-it',
'accounts/fireworks/models/gemma-3-12b-it',
'accounts/fireworks/models/gemma-3-1b-it',
'accounts/fireworks/models/gemma-3-27b-it',
'accounts/fireworks/models/gemma-3-4b-it',
'accounts/fireworks/models/gemma-4-26b-a4b-it',
'accounts/fireworks/models/gemma-4-31b-it',
'accounts/fireworks/models/gemma-4-31b-it-nvfp4',
'accounts/fireworks/models/gemma-4-e4b',
'accounts/fireworks/models/gemma-7b',
'accounts/fireworks/models/gemma-7b-it',
'accounts/fireworks/models/gemma2-9b-it',
'accounts/fireworks/models/glm-4p5',
'accounts/fireworks/models/glm-4p5-air',
'accounts/fireworks/models/glm-4p5v',
'accounts/fireworks/models/glm-4p6',
'accounts/fireworks/models/glm-4p7',
'accounts/fireworks/models/glm-4p7-flash',
'accounts/fireworks/models/glm-5',
'accounts/fireworks/models/glm-5p1',
'accounts/fireworks/models/gpt-oss-120b',
'accounts/fireworks/models/gpt-oss-20b',
'accounts/fireworks/models/gpt-oss-safeguard-120b',
'accounts/fireworks/models/gpt-oss-safeguard-20b',
'accounts/fireworks/models/hermes-2-pro-mistral-7b',
'accounts/fireworks/models/internvl3-38b',
'accounts/fireworks/models/internvl3-78b',
'accounts/fireworks/models/internvl3-8b',
'accounts/fireworks/models/kat-coder',
'accounts/fireworks/models/kat-dev-32b',
'accounts/fireworks/models/kat-dev-72b-exp',
'accounts/fireworks/models/kimi-k2-instruct',
'accounts/fireworks/models/kimi-k2-instruct-0905',
'accounts/fireworks/models/kimi-k2-thinking',
'accounts/fireworks/models/kimi-k2p5',
'accounts/fireworks/models/kimi-k2p6',
'accounts/fireworks/models/llama4-maverick-instruct-basic',
'accounts/fireworks/models/llama4-scout-instruct-basic',
'accounts/fireworks/models/llama-guard-2-8b',
'accounts/fireworks/models/llama-guard-3-1b',
'accounts/fireworks/models/llama-guard-3-8b',
'accounts/fireworks/models/llamaguard-7b',
'accounts/fireworks/models/llama-v2-13b',
'accounts/fireworks/models/llama-v2-13b-chat',
'accounts/fireworks/models/llama-v2-70b',
'accounts/fireworks/models/llama-v2-70b-chat',
'accounts/fireworks/models/llama-v2-7b',
'accounts/fireworks/models/llama-v2-7b-chat',
'accounts/fireworks/models/llama-v3-70b-instruct',
'accounts/fireworks/models/llama-v3-70b-instruct-hf',
'accounts/fireworks/models/llama-v3-8b',
'accounts/fireworks/models/llama-v3-8b-instruct',
'accounts/fireworks/models/llama-v3-8b-instruct-hf',
'accounts/fireworks/models/llama-v3p1-405b-instruct',
'accounts/fireworks/models/llama-v3p1-405b-instruct-long',
'accounts/fireworks/models/llama-v3p1-70b-instruct',
'accounts/fireworks/models/llama-v3p1-70b-instruct-1b',
'accounts/fireworks/models/llama-v3p1-8b-instruct',
'accounts/fireworks/models/llama-v3p1-nemotron-70b-instruct',
'accounts/fireworks/models/llama-v3p2-11b-vision-instruct',
'accounts/fireworks/models/llama-v3p2-1b',
'accounts/fireworks/models/llama-v3p2-1b-instruct',
'accounts/fireworks/models/llama-v3p2-3b',
'accounts/fireworks/models/llama-v3p2-3b-instruct',
'accounts/fireworks/models/llama-v3p2-90b-vision-instruct',
'accounts/fireworks/models/llama-v3p3-70b-instruct',
'accounts/fireworks/models/llava-yi-34b',
'accounts/fireworks/models/minimax-m1-80k',
'accounts/fireworks/models/minimax-m2',
'accounts/fireworks/models/minimax-m2p1',
'accounts/fireworks/models/minimax-m2p5',
'accounts/fireworks/models/minimax-m2p7',
'accounts/fireworks/models/ministral-3-14b-instruct-2512',
'accounts/fireworks/models/ministral-3-3b-instruct-2512',
'accounts/fireworks/models/ministral-3-8b-instruct-2512',
'accounts/fireworks/models/mirothinker-1p7',
'accounts/fireworks/models/mistral-7b',
'accounts/fireworks/models/mistral-7b-instruct-4k',
'accounts/fireworks/models/mistral-7b-instruct-v0p2',
'accounts/fireworks/models/mistral-7b-instruct-v3',
'accounts/fireworks/models/mistral-7b-v0p2',
'accounts/fireworks/models/mistral-large-3-fp8',
'accounts/fireworks/models/mistral-nemo-base-2407',
'accounts/fireworks/models/mistral-nemo-instruct-2407',
'accounts/fireworks/models/mistral-small-24b-instruct-2501',
'accounts/fireworks/models/mixtral-8x22b',
'accounts/fireworks/models/mixtral-8x22b-instruct',
'accounts/fireworks/models/mixtral-8x7b',
'accounts/fireworks/models/mixtral-8x7b-instruct',
'accounts/fireworks/models/mixtral-8x7b-instruct-hf',
'accounts/fireworks/models/molmo2-4b',
'accounts/fireworks/models/molmo2-8b',
'accounts/fireworks/models/mythomax-l2-13b',
'accounts/fireworks/models/nemotron-3-super-120b-a12b-bf16',
'accounts/fireworks/models/nemotron-3-ultra-bf16',
'accounts/fireworks/models/nemotron-3-ultra-nvfp4',
'accounts/fireworks/models/nemotron-nano-3-30b-a3b',
'accounts/fireworks/models/nemotron-nano-v2-12b-vl',
'accounts/fireworks/models/nvidia-nemotron-3-nano-omni-30b-a3b',
'accounts/fireworks/models/nvidia-nemotron-3-super-120b-a12b-bf16',
'accounts/fireworks/models/nvidia-nemotron-3-super-120b-a12b-fp8',
'accounts/fireworks/models/nvidia-nemotron-3-super-120b-a12b-nvfp4',
'accounts/fireworks/models/nvidia-nemotron-nano-12b-v2',
'accounts/fireworks/models/nvidia-nemotron-nano-9b-v2',
'accounts/fireworks/models/nous-capybara-7b-v1p9',
'accounts/fireworks/models/nous-hermes-2-mixtral-8x7b-dpo',
'accounts/fireworks/models/nous-hermes-2-yi-34b',
'accounts/fireworks/models/nous-hermes-llama2-13b',
'accounts/fireworks/models/nous-hermes-llama2-70b',
'accounts/fireworks/models/nous-hermes-llama2-7b',
'accounts/fireworks/models/openchat-3p5-0106-7b',
'accounts/fireworks/models/openhermes-2-mistral-7b',
'accounts/fireworks/models/openhermes-2p5-mistral-7b',
'accounts/fireworks/models/openorca-7b',
'accounts/fireworks/models/phi-2-3b',
'accounts/fireworks/models/phi-3-mini-128k-instruct',
'accounts/fireworks/models/phi-3-vision-128k-instruct',
'accounts/fireworks/models/phind-code-llama-34b-python-v1',
'accounts/fireworks/models/phind-code-llama-34b-v1',
'accounts/fireworks/models/phind-code-llama-34b-v2',
'accounts/fireworks/models/pythia-12b',
'accounts/fireworks/models/qwen-qwq-32b-preview',
'accounts/fireworks/models/qwen-v2p5-14b-instruct',
'accounts/fireworks/models/qwen-v2p5-7b',
'accounts/fireworks/models/qwen1p5-72b-chat',
'accounts/fireworks/models/qwen2-72b-instruct',
'accounts/fireworks/models/qwen2-7b-instruct',
'accounts/fireworks/models/qwen2-vl-2b-instruct',
'accounts/fireworks/models/qwen2-vl-72b-instruct',
'accounts/fireworks/models/qwen2-vl-7b-instruct',
'accounts/fireworks/models/qwen2p5-0p5b-instruct',
'accounts/fireworks/models/qwen2p5-14b',
'accounts/fireworks/models/qwen2p5-14b-instruct',
'accounts/fireworks/models/qwen2p5-1p5b-instruct',
'accounts/fireworks/models/qwen2p5-32b',
'accounts/fireworks/models/qwen2p5-32b-instruct',
'accounts/fireworks/models/qwen2p5-72b',
'accounts/fireworks/models/qwen2p5-72b-instruct',
'accounts/fireworks/models/qwen2p5-7b-instruct',
'accounts/fireworks/models/qwen2p5-coder-0p5b',
'accounts/fireworks/models/qwen2p5-coder-0p5b-instruct',
'accounts/fireworks/models/qwen2p5-coder-14b',
'accounts/fireworks/models/qwen2p5-coder-14b-instruct',
'accounts/fireworks/models/qwen2p5-coder-1p5b',
'accounts/fireworks/models/qwen2p5-coder-1p5b-instruct',
'accounts/fireworks/models/qwen2p5-coder-32b',
'accounts/fireworks/models/qwen2p5-coder-32b-instruct',
'accounts/fireworks/models/qwen2p5-coder-32b-instruct-128k',
'accounts/fireworks/models/qwen2p5-coder-32b-instruct-32k-rope',
'accounts/fireworks/models/qwen2p5-coder-32b-instruct-64k',
'accounts/fireworks/models/qwen2p5-coder-3b',
'accounts/fireworks/models/qwen2p5-coder-3b-instruct',
'accounts/fireworks/models/qwen2p5-coder-7b',
'accounts/fireworks/models/qwen2p5-coder-7b-instruct',
'accounts/fireworks/models/qwen2p5-math-72b-instruct',
'accounts/fireworks/models/qwen2p5-vl-32b-instruct',
'accounts/fireworks/models/qwen2p5-vl-3b-instruct',
'accounts/fireworks/models/qwen2p5-vl-72b-instruct',
'accounts/fireworks/models/qwen2p5-vl-7b-instruct',
'accounts/fireworks/models/qwen3-0p6b',
'accounts/fireworks/models/qwen3-14b',
'accounts/fireworks/models/qwen3-1p7b',
'accounts/fireworks/models/qwen3-1p7b-fp8-draft',
'accounts/fireworks/models/qwen3-1p7b-fp8-draft-131072',
'accounts/fireworks/models/qwen3-1p7b-fp8-draft-40960',
'accounts/fireworks/models/qwen3-235b-a22b',
'accounts/fireworks/models/qwen3-235b-a22b-instruct-2507',
'accounts/fireworks/models/qwen3-30b-a3b',
'accounts/fireworks/models/qwen3-30b-a3b-instruct-2507',
'accounts/fireworks/models/qwen3-30b-a3b-thinking-2507',
'accounts/fireworks/models/qwen3-32b',
'accounts/fireworks/models/qwen3-4b',
'accounts/fireworks/models/qwen3-4b-instruct-2507',
'accounts/fireworks/models/qwen3-8b',
'accounts/fireworks/models/qwen3-coder-30b-a3b-instruct',
'accounts/fireworks/models/qwen3-coder-480b-a35b-instruct',
'accounts/fireworks/models/qwen3-coder-480b-instruct-bf16',
'accounts/fireworks/models/qwen3-embedding-0p6b',
'accounts/fireworks/models/qwen3-embedding-4b',
'accounts/fireworks/models/qwen3-embedding-8b',
'accounts/fireworks/models/qwen3-next-80b-a3b-instruct',
'accounts/fireworks/models/qwen3-next-80b-a3b-thinking',
'accounts/fireworks/models/qwen3-omni-30b-a3b-instruct',
'accounts/fireworks/models/qwen3-reranker-0p6b',
'accounts/fireworks/models/qwen3-reranker-4b',
'accounts/fireworks/models/qwen3-reranker-8b',
'accounts/fireworks/models/qwen3-vl-235b-a22b-instruct',
'accounts/fireworks/models/qwen3-vl-235b-a22b-thinking',
'accounts/fireworks/models/qwen3-vl-30b-a3b-instruct',
'accounts/fireworks/models/qwen3-vl-30b-a3b-thinking',
'accounts/fireworks/models/qwen3-vl-32b-instruct',
'accounts/fireworks/models/qwen3-vl-8b-instruct',
'accounts/fireworks/models/qwen3p5-122b-a10b',
'accounts/fireworks/models/qwen3p5-27b',
'accounts/fireworks/models/qwen3p5-35b-a3b',
'accounts/fireworks/models/qwen3p5-397b-a17b',
'accounts/fireworks/models/qwen3p5-9b',
'accounts/fireworks/models/qwen3p6-27b',
'accounts/fireworks/models/qwen3p6-35b-a3b',
'accounts/fireworks/models/qwen3p6-plus',
'accounts/fireworks/models/qwen3p7-plus-vl-instruct',
'accounts/fireworks/models/qwq-32b',
'accounts/fireworks/models/rolm-ocr',
'accounts/fireworks/models/seed-oss-36b-instruct',
'accounts/fireworks/models/snorkel-mistral-7b-pairrm-dpo',
'accounts/fireworks/models/stablecode-3b',
'accounts/fireworks/models/starcoder-16b',
'accounts/fireworks/models/starcoder-7b',
'accounts/fireworks/models/starcoder2-15b',
'accounts/fireworks/models/starcoder2-3b',
'accounts/fireworks/models/starcoder2-7b',
'accounts/fireworks/models/step-3p7-flash-nvfp4',
'accounts/fireworks/models/toppy-m-7b',
'accounts/fireworks/models/voyage-4-nano',
'accounts/fireworks/models/yi-34b-200k-capybara',
'accounts/fireworks/models/zephyr-7b-beta',
],
})
+1
View File
@@ -43,6 +43,7 @@ const EXPECTED_PRESETS = [
'atomic-chat',
'gitlawb-opengateway',
'nearai',
'fireworks',
'opencode',
'opencode-go',
] as const satisfies readonly ProviderPreset[]
@@ -6,6 +6,7 @@ import vendorAnthropic from '../vendors/anthropic.js'
import vendorAtlasCloud from '../vendors/atlas-cloud.js'
import vendorBankr from '../vendors/bankr.js'
import vendorDeepseek from '../vendors/deepseek.js'
import vendorFireworks from '../vendors/fireworks.js'
import vendorGemini from '../vendors/gemini.js'
import vendorMinimax from '../vendors/minimax.js'
import vendorMoonshot from '../vendors/moonshot.js'
@@ -37,6 +38,7 @@ import gatewayTogether from '../gateways/together.js'
import gatewayVertex from '../gateways/vertex.js'
import brandClaude from '../brands/claude.js'
import brandDeepseek from '../brands/deepseek.js'
import brandFireworks from '../brands/fireworks.js'
import brandGemini from '../brands/gemini.js'
import brandGlm from '../brands/glm.js'
import brandGpt from '../brands/gpt.js'
@@ -52,6 +54,7 @@ import brandXai from '../brands/xai.js'
import brandXiaomiMimo from '../brands/xiaomi-mimo.js'
import modelClaude from '../models/claude.js'
import modelDeepseek from '../models/deepseek.js'
import modelFireworksMerged from '../models/fireworks-merged.js'
import modelGemini from '../models/gemini.js'
import modelGlm from '../models/glm.js'
import modelGpt from '../models/gpt.js'
@@ -67,11 +70,11 @@ import modelQwen from '../models/qwen.js'
import modelXai from '../models/xai.js'
import modelXiaomiMimo from '../models/xiaomi-mimo.js'
export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorAtlasCloud, vendorBankr, vendorDeepseek, vendorGemini, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorAtlasCloud, vendorBankr, vendorDeepseek, vendorFireworks, vendorGemini, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex] as const satisfies readonly GatewayDescriptor[]
export const ANTHROPIC_PROXY_DESCRIPTORS = [] as const satisfies readonly AnthropicProxyDescriptor[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandGemini, brandGlm, brandGpt, brandKimi, brandLlama, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelGemini, modelGlm, modelGpt, modelKimi, modelLlama, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLlama, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLlama, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
export const MODEL_DESCRIPTORS = MODEL_DESCRIPTOR_GROUPS.flat() satisfies readonly ModelDescriptor[]
export const PROVIDER_PRESET_MANIFEST = [
@@ -188,6 +191,19 @@ export const PROVIDER_PRESET_MANIFEST = [
"DEEPSEEK_API_KEY"
]
},
{
"preset": "fireworks",
"routeKind": "vendor",
"routeId": "fireworks",
"vendorId": "fireworks",
"description": "Fireworks AI OpenAI-compatible endpoint",
"apiKeyEnvVars": [
"FIREWORKS_API_KEY"
],
"modelEnvVars": [
"OPENAI_MODEL"
]
},
{
"preset": "gemini",
"routeKind": "vendor",
@@ -476,6 +492,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"azure-openai",
"bankr",
"deepseek",
"fireworks",
"gemini",
"groq",
"hicap",
File diff suppressed because it is too large Load Diff
+63 -1
View File
@@ -316,6 +316,24 @@ export function isNearaiBaseUrl(value: string | undefined): boolean {
return false
}
}
/**
* Checks whether the given URL value targets the Fireworks AI inference API
* by matching the hostname against `api.fireworks.ai`.
*/
export function isFireworksBaseUrl(value: string | undefined): boolean {
const trimmed = value?.trim()
if (!trimmed) {
return false
}
try {
return new URL(trimmed).hostname.toLowerCase() === 'api.fireworks.ai'
} catch {
return false
}
}
export function getNearaiBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
@@ -332,6 +350,25 @@ export function getNearaiBaseUrlOverride(
return undefined
}
/**
* Returns the user-configured Fireworks AI base URL from `OPENAI_BASE_URL`
* or `OPENAI_API_BASE` when the hostname resolves to api.fireworks.ai.
*/
export function getFireworksBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
const openAIBaseUrl = processEnv.OPENAI_BASE_URL?.trim()
if (isFireworksBaseUrl(openAIBaseUrl)) {
return openAIBaseUrl
}
const openAIApiBase = processEnv.OPENAI_API_BASE?.trim()
if (isFireworksBaseUrl(openAIApiBase)) {
return openAIApiBase
}
return undefined
}
export function getMiniMaxBaseUrlOverride(
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
@@ -478,14 +515,35 @@ export function hasNearaiEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MIMO_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isNearaiBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
}
/**
* Detects whether the process environment is configured to route traffic
* exclusively through Fireworks AI based on the presence of FIREWORKS_API_KEY
* and the absence of conflicting env vars for other providers.
*/
export function hasFireworksEnvOnlyProviderIntent(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MIMO_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isFireworksBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
}
export function resolveEnvOnlyProviderRouteId(
processEnv: NodeJS.ProcessEnv = process.env,
): 'xai' | 'minimax' | 'venice' | 'xiaomi-mimo' | 'nearai' | null {
): 'xai' | 'minimax' | 'venice' | 'xiaomi-mimo' | 'nearai' | 'fireworks' | null {
if (
hasMiniMaxRouteIntent(processEnv) &&
hasMiniMaxEnvOnlyProviderIntent(processEnv)
@@ -513,6 +571,10 @@ export function resolveEnvOnlyProviderRouteId(
return 'nearai'
}
if (hasFireworksEnvOnlyProviderIntent(processEnv)) {
return 'fireworks'
}
return null
}
File diff suppressed because it is too large Load Diff
+306
View File
@@ -35,6 +35,10 @@ const originalEnv = {
OPENAI_MODEL: process.env.OPENAI_MODEL,
MINIMAX_API_KEY: process.env.MINIMAX_API_KEY,
XAI_API_KEY: process.env.XAI_API_KEY,
FIREWORKS_API_KEY: process.env.FIREWORKS_API_KEY,
OPENAI_AUTH_HEADER: process.env.OPENAI_AUTH_HEADER,
OPENAI_AUTH_SCHEME: process.env.OPENAI_AUTH_SCHEME,
OPENAI_AUTH_HEADER_VALUE: process.env.OPENAI_AUTH_HEADER_VALUE,
NVIDIA_NIM: process.env.NVIDIA_NIM,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
@@ -76,6 +80,10 @@ function clearEnvForMiniMaxOnlyTest(): void {
delete process.env.OPENAI_API_BASE
delete process.env.OPENAI_MODEL
delete process.env.XAI_API_KEY
delete process.env.FIREWORKS_API_KEY
delete process.env.OPENAI_AUTH_HEADER
delete process.env.OPENAI_AUTH_SCHEME
delete process.env.OPENAI_AUTH_HEADER_VALUE
delete process.env.NVIDIA_NIM
delete process.env.ANTHROPIC_API_KEY
delete process.env.ANTHROPIC_AUTH_TOKEN
@@ -108,6 +116,10 @@ beforeEach(async () => {
delete process.env.OPENAI_MODEL
delete process.env.MINIMAX_API_KEY
delete process.env.XAI_API_KEY
delete process.env.FIREWORKS_API_KEY
delete process.env.OPENAI_AUTH_HEADER
delete process.env.OPENAI_AUTH_SCHEME
delete process.env.OPENAI_AUTH_HEADER_VALUE
delete process.env.NVIDIA_NIM
delete process.env.ANTHROPIC_API_KEY
delete process.env.ANTHROPIC_AUTH_TOKEN
@@ -144,6 +156,10 @@ afterEach(() => {
restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL)
restoreEnv('MINIMAX_API_KEY', originalEnv.MINIMAX_API_KEY)
restoreEnv('XAI_API_KEY', originalEnv.XAI_API_KEY)
restoreEnv('FIREWORKS_API_KEY', originalEnv.FIREWORKS_API_KEY)
restoreEnv('OPENAI_AUTH_HEADER', originalEnv.OPENAI_AUTH_HEADER)
restoreEnv('OPENAI_AUTH_SCHEME', originalEnv.OPENAI_AUTH_SCHEME)
restoreEnv('OPENAI_AUTH_HEADER_VALUE', originalEnv.OPENAI_AUTH_HEADER_VALUE)
restoreEnv('NVIDIA_NIM', originalEnv.NVIDIA_NIM)
restoreEnv('ANTHROPIC_API_KEY', originalEnv.ANTHROPIC_API_KEY)
restoreEnv('ANTHROPIC_AUTH_TOKEN', originalEnv.ANTHROPIC_AUTH_TOKEN)
@@ -869,6 +885,296 @@ test('env-only xAI fallback yields to explicit Bedrock selection', async () => {
expect(process.env.OPENAI_API_KEY).toBeUndefined()
})
test('routes env-only Fireworks AI requests through the OpenAI-compatible shim', async () => {
let capturedUrl: string | undefined
let capturedHeaders: Headers | undefined
let capturedBody: Record<string, unknown> | undefined
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.GEMINI_API_KEY
delete process.env.GEMINI_MODEL
delete process.env.GEMINI_BASE_URL
delete process.env.GEMINI_AUTH_MODE
process.env.FIREWORKS_API_KEY = 'fireworks-test-key'
globalThis.fetch = (async (input, init) => {
capturedUrl =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url
capturedHeaders = new Headers(init?.headers)
capturedBody = JSON.parse(String(init?.body)) as Record<string, unknown>
return new Response(
JSON.stringify({
id: 'chatcmpl-fw',
model: 'accounts/fireworks/models/deepseek-v3',
choices: [
{
message: { role: 'assistant', content: 'fireworks ok' },
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 8,
completion_tokens: 3,
total_tokens: 11,
},
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as FetchType
const client = (await getAnthropicClient({
maxRetries: 0,
model: 'accounts/fireworks/models/deepseek-v3',
})) as unknown as ShimClient
const response = await client.beta.messages.create({
model: 'accounts/fireworks/models/deepseek-v3',
system: 'test system',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 64,
stream: false,
})
expect(capturedUrl).toBe(
'https://api.fireworks.ai/inference/v1/chat/completions',
)
expect(capturedHeaders?.get('authorization')).toBe(
'Bearer fireworks-test-key',
)
expect(capturedBody?.model).toBe('accounts/fireworks/models/deepseek-v3')
expect(response).toMatchObject({
role: 'assistant',
model: 'accounts/fireworks/models/deepseek-v3',
})
})
test('env-only Fireworks fallback replaces stale OpenAI model env', async () => {
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.GEMINI_API_KEY
delete process.env.GEMINI_MODEL
delete process.env.GEMINI_BASE_URL
delete process.env.GEMINI_AUTH_MODE
delete process.env.ANTHROPIC_API_KEY
delete process.env.ANTHROPIC_AUTH_TOKEN
process.env.FIREWORKS_API_KEY = 'fireworks-test-key'
process.env.OPENAI_MODEL = 'gpt-4o'
await getAnthropicClient({ maxRetries: 0, model: 'accounts/fireworks/models/deepseek-v3' })
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1')
expect(process.env.OPENAI_MODEL).toBe(
'accounts/fireworks/models/llama-v3p1-70b-instruct',
)
expect(process.env.OPENAI_API_KEY).toBe('fireworks-test-key')
})
test('env-only Fireworks fallback preserves Fireworks OPENAI_API_BASE host overrides', async () => {
let capturedUrl: string | undefined
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.GEMINI_API_KEY
delete process.env.GEMINI_MODEL
delete process.env.GEMINI_BASE_URL
delete process.env.GEMINI_AUTH_MODE
delete process.env.ANTHROPIC_API_KEY
delete process.env.ANTHROPIC_AUTH_TOKEN
delete process.env.ANTHROPIC_BASE_URL
delete process.env.OPENAI_BASE_URL
process.env.FIREWORKS_API_KEY = 'fireworks-test-key'
process.env.OPENAI_API_BASE = 'https://api.fireworks.ai/inference/v1'
globalThis.fetch = (async (input, init) => {
capturedUrl =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url
return new Response(
JSON.stringify({
id: 'chatcmpl-fw-api-base',
model: 'accounts/fireworks/models/deepseek-v3',
choices: [
{
message: {
role: 'assistant',
content: 'fireworks api base ok',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 8,
completion_tokens: 3,
total_tokens: 11,
},
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as FetchType
const client = (await getAnthropicClient({
maxRetries: 0,
model: 'accounts/fireworks/models/deepseek-v3',
})) as unknown as ShimClient
await client.beta.messages.create({
model: 'accounts/fireworks/models/deepseek-v3',
system: 'test system',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 64,
stream: false,
})
expect(capturedUrl).toBe(
'https://api.fireworks.ai/inference/v1/chat/completions',
)
expect(String(process.env.OPENAI_BASE_URL)).toBe('https://api.fireworks.ai/inference/v1')
})
test('env-only Fireworks fallback drops unsupported OpenAI shim options', async () => {
let capturedHeaders: Headers | undefined
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.GEMINI_API_KEY
delete process.env.GEMINI_MODEL
delete process.env.GEMINI_BASE_URL
delete process.env.GEMINI_AUTH_MODE
process.env.FIREWORKS_API_KEY = 'fireworks-test-key'
process.env.OPENAI_API_FORMAT = 'responses'
process.env.OPENAI_AUTH_HEADER = 'api-key'
process.env.OPENAI_AUTH_SCHEME = 'raw'
process.env.OPENAI_AUTH_HEADER_VALUE = 'stale-header-value'
globalThis.fetch = (async (input, init) => {
capturedHeaders = new Headers(init?.headers)
return new Response(
JSON.stringify({
id: 'chatcmpl-fw-clean',
model: 'accounts/fireworks/models/deepseek-v3',
choices: [
{
message: {
role: 'assistant',
content: 'fireworks clean ok',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 8,
completion_tokens: 3,
total_tokens: 11,
},
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as FetchType
const client = (await getAnthropicClient({
maxRetries: 0,
model: 'accounts/fireworks/models/deepseek-v3',
})) as unknown as ShimClient
await client.beta.messages.create({
model: 'accounts/fireworks/models/deepseek-v3',
system: 'test system',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 64,
stream: false,
})
expect(capturedHeaders?.get('authorization')).toBe(
'Bearer fireworks-test-key',
)
expect(capturedHeaders?.get('api-key')).toBeNull()
expect(process.env.OPENAI_API_FORMAT).toBeUndefined()
expect(process.env.OPENAI_AUTH_HEADER).toBeUndefined()
expect(process.env.OPENAI_AUTH_SCHEME).toBeUndefined()
expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBeUndefined()
})
test('env-only Fireworks fallback ignores non-Fireworks base overrides', async () => {
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.GEMINI_API_KEY
delete process.env.GEMINI_MODEL
delete process.env.GEMINI_BASE_URL
delete process.env.GEMINI_AUTH_MODE
process.env.ANTHROPIC_API_KEY = 'anthropic-test-key'
process.env.FIREWORKS_API_KEY = 'fireworks-test-key'
process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1'
process.env.OPENAI_MODEL = 'accounts/fireworks/models/deepseek-v3'
await getAnthropicClient({ maxRetries: 0, model: 'accounts/fireworks/models/deepseek-v3' })
// ANTHROPIC_API_KEY takes precedence — Fireworks env-only provider does not activate
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined()
expect(process.env.OPENAI_API_KEY).toBeUndefined()
expect(process.env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1')
expect(process.env.OPENAI_MODEL).toBe(
'accounts/fireworks/models/deepseek-v3',
)
})
test('env-only Fireworks does not activate when MiniMax key is present', async () => {
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.GEMINI_API_KEY
delete process.env.GEMINI_MODEL
delete process.env.GEMINI_BASE_URL
delete process.env.GEMINI_AUTH_MODE
delete process.env.ANTHROPIC_API_KEY
delete process.env.ANTHROPIC_AUTH_TOKEN
process.env.MINIMAX_API_KEY = 'minimax-test-key'
process.env.FIREWORKS_API_KEY = 'fireworks-test-key'
globalThis.fetch = (async () => {
throw new Error('Fireworks/OpenAI shim fetch should not run')
}) as unknown as FetchType
await getAnthropicClient({
maxRetries: 0,
model: 'claude-sonnet-4-6',
})
// MiniMax takes priority over Fireworks
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined()
expect(process.env.OPENAI_BASE_URL).toBeUndefined()
expect(process.env.OPENAI_MODEL).toBeUndefined()
expect(process.env.OPENAI_API_KEY).toBeUndefined()
})
test('env-only Fireworks fallback yields to explicit Bedrock selection', async () => {
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.GEMINI_API_KEY
delete process.env.GEMINI_MODEL
delete process.env.GEMINI_BASE_URL
delete process.env.GEMINI_AUTH_MODE
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH = '1'
process.env.FIREWORKS_API_KEY = 'fireworks-test-key'
globalThis.fetch = (async () => {
throw new Error('Fireworks/OpenAI shim fetch should not run')
}) as unknown as FetchType
await getAnthropicClient({
maxRetries: 0,
model: 'claude-sonnet-4-6',
})
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined()
expect(process.env.OPENAI_BASE_URL).toBeUndefined()
expect(process.env.OPENAI_MODEL).toBeUndefined()
expect(process.env.OPENAI_API_KEY).toBeUndefined()
})
test('strips Anthropic-specific custom headers before sending OpenAI-compatible shim requests', async () => {
let capturedHeaders: Headers | undefined
+43
View File
@@ -35,6 +35,7 @@ import {
isEnvTruthy,
} from '../../utils/envUtils.js'
import {
getFireworksBaseUrlOverride,
getMiniMaxBaseUrlOverride,
getNearaiBaseUrlOverride,
getRouteDefaultBaseUrl,
@@ -272,6 +273,42 @@ function applyNearaiEnvOnlyDefaults(): void {
delete process.env.OPENAI_AUTH_HEADER_VALUE
}
/**
* Checks whether the given model ID follows the Fireworks AI model naming
* convention (`accounts/fireworks/models/...`).
*/
function isFireworksModelName(value: string | undefined): boolean {
const normalized = value?.trim().toLowerCase()
return Boolean(
normalized && normalized.startsWith('accounts/fireworks/models/'),
)
}
/**
* Applies Fireworks AI environment defaults by setting the OpenAI shim env
* vars (`CLAUDE_CODE_USE_OPENAI`, `OPENAI_BASE_URL`, `OPENAI_MODEL`,
* `OPENAI_API_KEY`) and clearing stale OpenAI shim options.
*/
function applyFireworksEnvOnlyDefaults(): void {
const baseUrlOverride = getFireworksBaseUrlOverride()
const hasFireworksBaseOverride = baseUrlOverride !== undefined
const modelOverride = process.env.OPENAI_MODEL?.trim() || undefined
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL =
baseUrlOverride ?? getRouteDefaultBaseUrl('fireworks')
process.env.OPENAI_MODEL =
(hasFireworksBaseOverride || isFireworksModelName(modelOverride)
? modelOverride
: undefined) ??
getRouteDefaultModel('fireworks')
process.env.OPENAI_API_KEY = process.env.FIREWORKS_API_KEY
delete process.env.OPENAI_API_FORMAT
delete process.env.OPENAI_AUTH_HEADER
delete process.env.OPENAI_AUTH_SCHEME
delete process.env.OPENAI_AUTH_HEADER_VALUE
}
export async function getAnthropicClient({
apiKey,
maxRetries,
@@ -336,6 +373,8 @@ export async function getAnthropicClient({
envOnlyProviderRouteId === 'xai' && !useMiniMaxEnvOnlyProvider
const useNearaiEnvOnlyProvider =
envOnlyProviderRouteId === 'nearai' && !useMiniMaxEnvOnlyProvider
const useFireworksEnvOnlyProvider =
envOnlyProviderRouteId === 'fireworks' && !useMiniMaxEnvOnlyProvider
if (useMiniMaxEnvOnlyProvider) {
applyMiniMaxEnvOnlyDefaults(model)
}
@@ -348,6 +387,9 @@ export async function getAnthropicClient({
if (useNearaiEnvOnlyProvider) {
applyNearaiEnvOnlyDefaults()
}
if (useFireworksEnvOnlyProvider) {
applyFireworksEnvOnlyDefaults()
}
const shouldUseFirstPartyAuth =
shouldUseFirstPartyAnthropicAuth(providerOverride)
@@ -425,6 +467,7 @@ export async function getAnthropicClient({
useXiaomiMimoEnvOnlyProvider ||
useXaiEnvOnlyProvider ||
useNearaiEnvOnlyProvider ||
useFireworksEnvOnlyProvider ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
+7
View File
@@ -124,6 +124,13 @@ describe('detectProviderFromEnv — priority order', () => {
})
})
test('FIREWORKS_API_KEY detected', () => {
expect(scan({ FIREWORKS_API_KEY: 'fw-key' })).toEqual({
kind: 'fireworks',
source: 'FIREWORKS_API_KEY set',
})
})
test('empty-string values are ignored', () => {
expect(
scan({
+12 -3
View File
@@ -16,11 +16,12 @@
* 5. GEMINI_API_KEY or GOOGLE_API_KEY
* 6. MISTRAL_API_KEY
* 7. MINIMAX_API_KEY
* 8. MIMO_API_KEY
* 8. MIMO_API_KEY (Xiaomi Mimo)
* 9. XAI_API_KEY
* 10. NEARAI_API_KEY
* 11. Local Ollama reachable (default localhost:11434)
* 12. Local LM Studio reachable (default localhost:1234)
* 11. FIREWORKS_API_KEY
* 12. Local Ollama reachable (default localhost:11434)
* 13. Local LM Studio reachable (default localhost:1234)
*
* Local-service probes are parallelized and cheap (short timeout, no
* request body). Env scans are synchronous and run first so we don't make
@@ -46,6 +47,7 @@ export type DetectedProviderKind =
| 'xiaomi-mimo'
| 'xai'
| 'nearai'
| 'fireworks'
| 'ollama'
| 'lm-studio'
| 'gitlawb-opengateway'
@@ -181,6 +183,13 @@ export function detectProviderFromEnv(
}
}
if (envHasNonEmpty(env, 'FIREWORKS_API_KEY')) {
return {
kind: 'fireworks',
source: 'FIREWORKS_API_KEY set',
}
}
return null
}
+27 -9
View File
@@ -42,6 +42,7 @@ const PREFERRED_PROVIDER_ORDER = [
'venice',
'atlas-cloud',
'nearai',
'fireworks',
] as const
function buildValidProviders(): string[] {
@@ -292,6 +293,9 @@ export function applyProviderFlag(
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.NEARAI_API_KEY
? 'nearai'
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.FIREWORKS_API_KEY
? 'fireworks'
: process.env.OPENAI_API_KEY !== undefined &&
opengatewayApiKey !== undefined &&
opengatewayApiKey.length > 0 &&
@@ -426,15 +430,6 @@ export function applyProviderFlag(
}
break
default:
process.env.CLAUDE_CODE_USE_OPENAI = '1'
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
if (defaultModel) {
process.env.OPENAI_MODEL ??= defaultModel
}
if (model) process.env.OPENAI_MODEL = model
break
case 'xai':
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL ??= 'https://api.x.ai/v1'
@@ -483,6 +478,29 @@ export function applyProviderFlag(
delete process.env.OPENAI_API_KEY
}
break
case 'fireworks':
process.env.CLAUDE_CODE_USE_OPENAI = '1'
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
if (defaultModel) {
process.env.OPENAI_MODEL ??= defaultModel
}
if (model) process.env.OPENAI_MODEL = model
if (process.env.FIREWORKS_API_KEY) {
process.env.OPENAI_API_KEY = process.env.FIREWORKS_API_KEY
} else {
delete process.env.OPENAI_API_KEY
}
break
default:
process.env.CLAUDE_CODE_USE_OPENAI = '1'
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
if (defaultModel) {
process.env.OPENAI_MODEL ??= defaultModel
}
if (model) process.env.OPENAI_MODEL = model
break
}
return {}
+36
View File
@@ -311,6 +311,42 @@ test('buildStartupEnvFromProfile preserves explicit OpenAI-compatible env withou
assert.equal(isDefaultStartupProviderEnv(env), false)
})
test('buildStartupEnvFromProfile preserves env-only Fireworks setup without a saved profile', async () => {
const env = await buildStartupEnvFromProfile({
persisted: null,
processEnv: {
FIREWORKS_API_KEY: 'fw-key',
},
})
// Must NOT fall through to Gitlawb Opengateway default
assert.equal(env.FIREWORKS_API_KEY, 'fw-key')
assert.equal(env.CLAUDE_CODE_USE_OPENAI, undefined)
assert.equal(
env.OPENAI_BASE_URL,
undefined,
'should not inject Gitlawb Opengateway base URL',
)
assert.equal(isDefaultStartupProviderEnv(env), false)
})
test('buildStartupEnvFromProfile preserves env-only NEAR AI setup without a saved profile', async () => {
const env = await buildStartupEnvFromProfile({
persisted: null,
processEnv: {
NEARAI_API_KEY: 'nearai-key',
},
})
assert.equal(env.NEARAI_API_KEY, 'nearai-key')
assert.equal(
env.OPENAI_BASE_URL,
undefined,
'should not inject Gitlawb Opengateway base URL',
)
assert.equal(isDefaultStartupProviderEnv(env), false)
})
test('openai launch preserves shell responses format and custom auth overrides', async () => {
const env = await buildLaunchEnv({
profile: 'openai',
+15 -2
View File
@@ -99,6 +99,7 @@ const PROFILE_ENV_KEYS = [
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'OPENCODE_API_KEY',
DEFAULT_STARTUP_PROVIDER_ENV_VAR,
] as const
@@ -127,6 +128,7 @@ const SECRET_ENV_KEYS = [
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'OPENCODE_API_KEY',
] as const
@@ -188,6 +190,7 @@ export type ProfileEnv = {
MIMO_API_KEY?: string
ATLAS_CLOUD_API_KEY?: string
NEARAI_API_KEY?: string
FIREWORKS_API_KEY?: string
OPENCODE_API_KEY?: string
}
@@ -213,7 +216,8 @@ type SecretValueSource = Partial<
| 'VENICE_API_KEY'
| 'MIMO_API_KEY'
| 'ATLAS_CLOUD_API_KEY'
| 'NEARAI_API_KEY',
| 'NEARAI_API_KEY'
| 'FIREWORKS_API_KEY',
string | undefined
>
>
@@ -1131,10 +1135,18 @@ function hasConcreteProviderSelection(
)
}
return (
if (
isEnvTruthy(processEnv.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(processEnv.CLAUDE_CODE_USE_VERTEX) ||
isEnvTruthy(processEnv.CLAUDE_CODE_USE_FOUNDRY)
) {
return true
}
// Env-only provider setups — no CLAUDE_CODE_USE_* flag needed
return (
sanitizeApiKey(processEnv.FIREWORKS_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.NEARAI_API_KEY) !== undefined
)
}
@@ -1634,6 +1646,7 @@ export async function buildLaunchEnv(options: {
for (const dedicatedKey of [
'ATLAS_CLOUD_API_KEY',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'MIMO_API_KEY',
'VENICE_API_KEY',
] as const) {
+119
View File
@@ -220,6 +220,17 @@ function buildXiaomiMimoProfile(overrides: Partial<ProviderProfile> = {}): Provi
})
}
function buildFireworksProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
return buildProfile({
provider: 'fireworks',
name: 'Fireworks AI',
baseUrl: 'https://api.fireworks.ai/inference/v1',
model: 'accounts/fireworks/models/deepseek-v3',
apiKey: 'fireworks-test-key',
...overrides,
})
}
function buildAtlasCloudProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
return buildProfile({
provider: 'atlas-cloud',
@@ -603,6 +614,28 @@ describe('applyProviderProfileToProcessEnv', () => {
expect(getFreshAPIProvider()).toBe('xiaomi-mimo')
})
test('fireworks profile applies OpenAI-compatible env with FIREWORKS_API_KEY mirror', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.CLAUDE_CODE_USE_GEMINI = '1'
applyProviderProfileToProcessEnv(buildFireworksProfile())
const { getAPIProvider: getFreshAPIProvider } =
await importFreshProvidersModule()
expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined()
expect(String(process.env.CLAUDE_CODE_USE_OPENAI)).toBe('1')
expect(process.env.OPENAI_BASE_URL).toBe(
'https://api.fireworks.ai/inference/v1',
)
expect(process.env.OPENAI_MODEL).toBe(
'accounts/fireworks/models/deepseek-v3',
)
expect(process.env.OPENAI_API_KEY).toBe('fireworks-test-key')
expect(process.env.FIREWORKS_API_KEY).toBe('fireworks-test-key')
expect(getFreshAPIProvider()).toBe('openai')
})
test('legacy OpenAI profile on restricted route ignores advanced settings', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
@@ -1019,6 +1052,24 @@ describe('applyActiveProviderProfileFromConfig', () => {
expect(String(process.env.XAI_API_KEY)).toBe('xai-test-key')
})
test('re-applies Fireworks AI active profile when FIREWORKS_API_KEY is missing (env drift)', async () => {
const { applyActiveProviderProfileFromConfig, applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
const fwProfile = buildFireworksProfile({ id: 'saved_fw' })
applyProviderProfileToProcessEnv(fwProfile)
// Simulate relaunch where the shell exported OPENAI vars but not FIREWORKS_API_KEY
delete process.env.FIREWORKS_API_KEY
const applied = applyActiveProviderProfileFromConfig({
providerProfiles: [fwProfile],
activeProviderProfileId: 'saved_fw',
} as any)
expect(applied?.id).toBe('saved_fw')
expect(String(process.env.FIREWORKS_API_KEY)).toBe('fireworks-test-key')
})
test('does not re-apply xai active profile when XAI_API_KEY is aligned', async () => {
const { applyActiveProviderProfileFromConfig, applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
@@ -1317,6 +1368,23 @@ describe('getProviderPresetDefaults', () => {
expect(defaults.model).toBe('GLM-5.1')
expect(defaults.requiresApiKey).toBe(true)
})
test('fireworks preset defaults to the official Fireworks AI endpoint', async () => {
const { getProviderPresetDefaults } =
await importFreshProviderProfileModules()
process.env.FIREWORKS_API_KEY = 'fireworks-live-key'
const defaults = getProviderPresetDefaults('fireworks')
expect(defaults.provider).toBe('fireworks')
expect(defaults.name).toBe('Fireworks AI')
expect(defaults.baseUrl).toBe('https://api.fireworks.ai/inference/v1')
expect(defaults.model).toBe(
'accounts/fireworks/models/llama-v3p1-70b-instruct',
)
expect(defaults.apiKey).toBe('fireworks-live-key')
expect(defaults.requiresApiKey).toBe(true)
})
})
describe('setActiveProviderProfile', () => {
@@ -1633,6 +1701,57 @@ describe('setActiveProviderProfile', () => {
}
})
test('persists Fireworks AI profiles using a legacy-compatible openai startup profile', async () => {
const tempDir = mkdtempSync(
join(tmpdir(), 'openclaude-provider-'),
)
const configDir = mkdtempSync(
join(tmpdir(), 'openclaude-provider-config-'),
)
process.chdir(tempDir)
process.env.CLAUDE_CONFIG_DIR = configDir
try {
const { setActiveProviderProfile } =
await importFreshProviderProfileModules()
const fireworksProfile = buildFireworksProfile({
id: 'fireworks_prof',
model: 'accounts/fireworks/models/deepseek-v3, accounts/fireworks/models/llama-v3p1-70b-instruct',
})
saveMockGlobalConfig(current => ({
...current,
providerProfiles: [fireworksProfile],
}))
const result = setActiveProviderProfile('fireworks_prof', {
configDir,
})
const persisted = JSON.parse(
readFileSync(
join(configDir, '.openclaude-profile.json'),
'utf8',
),
)
expect(result?.id).toBe('fireworks_prof')
expect(
existsSync(join(tempDir, '.openclaude-profile.json')),
).toBe(false)
expect(persisted.profile).toBe('openai')
expect(persisted.env).toEqual({
OPENAI_BASE_URL: 'https://api.fireworks.ai/inference/v1',
OPENAI_MODEL: 'accounts/fireworks/models/deepseek-v3',
OPENAI_API_KEY: 'fireworks-test-key',
FIREWORKS_API_KEY: 'fireworks-test-key',
})
} finally {
process.chdir(originalCwd)
rmSync(tempDir, { recursive: true, force: true })
rmSync(configDir, { recursive: true, force: true })
}
})
test('persists Xiaomi MiMo profiles using a legacy-compatible openai startup profile', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
+19 -13
View File
@@ -44,7 +44,7 @@ import {
type ResolvedProfileRoute,
type ProviderPreset,
} from '../integrations/index.js'
import { isNearaiBaseUrl, resolveEnvOnlyProviderRouteId } from '../integrations/routeMetadata.js'
import { isFireworksBaseUrl, isNearaiBaseUrl, resolveEnvOnlyProviderRouteId } from '../integrations/routeMetadata.js'
import { logForDebugging } from './debug.js'
import {
sanitizeProfileCustomHeaders,
@@ -557,6 +557,10 @@ function isProcessEnvAlignedWithProfile(
(isNearaiBaseUrl(profile.baseUrl)
? !includeApiKey ||
sameOptionalEnvValue(processEnv.NEARAI_API_KEY, profile.apiKey)
: true) &&
(isFireworksBaseUrl(profile.baseUrl)
? !includeApiKey ||
sameOptionalEnvValue(processEnv.FIREWORKS_API_KEY, profile.apiKey)
: true)
)
}
@@ -698,6 +702,9 @@ export function applyProviderProfileToProcessEnv(profile: ProviderProfile): void
if (route.routeId === 'nearai' || isNearaiBaseUrl(profile.baseUrl)) {
openAIProfileEnv.NEARAI_API_KEY = profile.apiKey
}
if (route.routeId === 'fireworks' || isFireworksBaseUrl(profile.baseUrl)) {
openAIProfileEnv.FIREWORKS_API_KEY = profile.apiKey
}
}
if (route.gatewayId === 'nvidia-nim') {
openAIProfileEnv.NVIDIA_NIM = '1'
@@ -946,6 +953,9 @@ function buildOpenAICompatibleStartupEnv(
if (isNearaiBaseUrl(activeProfile.baseUrl)) {
strictEnv.NEARAI_API_KEY = activeProfile.apiKey
}
if (isFireworksBaseUrl(activeProfile.baseUrl)) {
strictEnv.FIREWORKS_API_KEY = activeProfile.apiKey
}
return applySupportedProfileCustomHeaders(activeProfile, strictEnv)
}
}
@@ -953,19 +963,12 @@ function buildOpenAICompatibleStartupEnv(
const env: ProfileEnv = {
OPENAI_BASE_URL: activeProfile.baseUrl,
OPENAI_MODEL: getPrimaryModel(activeProfile.model),
...(activeProfile.apiFormat ? { OPENAI_API_FORMAT: activeProfile.apiFormat } : {}),
...(activeProfile.authHeader ? { OPENAI_AUTH_HEADER: activeProfile.authHeader } : {}),
...(activeProfile.authScheme ? { OPENAI_AUTH_SCHEME: activeProfile.authScheme } : {}),
...(activeProfile.authHeaderValue ? { OPENAI_AUTH_HEADER_VALUE: activeProfile.authHeaderValue } : {}),
}
if (activeProfile.apiFormat) {
env.OPENAI_API_FORMAT = activeProfile.apiFormat
}
if (activeProfile.authHeader) {
env.OPENAI_AUTH_HEADER = activeProfile.authHeader
env.OPENAI_AUTH_SCHEME = activeProfile.authScheme ?? (
activeProfile.authHeader.toLowerCase() === 'authorization' ? 'bearer' : 'raw'
)
if (activeProfile.authHeaderValue) {
env.OPENAI_AUTH_HEADER_VALUE = activeProfile.authHeaderValue
}
}
if (activeProfile.apiKey) {
env.OPENAI_API_KEY = activeProfile.apiKey
if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) {
@@ -989,6 +992,9 @@ function buildOpenAICompatibleStartupEnv(
if (isNearaiBaseUrl(activeProfile.baseUrl)) {
env.NEARAI_API_KEY = activeProfile.apiKey
}
if (isFireworksBaseUrl(activeProfile.baseUrl)) {
env.FIREWORKS_API_KEY = activeProfile.apiKey
}
} else {
delete env.OPENAI_API_KEY
}