mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
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:
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -119,6 +119,7 @@ const PRESET_ORDER = [
|
||||
'DeepSeek',
|
||||
'Codex OAuth',
|
||||
'xAI OAuth (Grok)',
|
||||
'Fireworks AI',
|
||||
'Google Gemini',
|
||||
'Groq',
|
||||
'Hicap',
|
||||
|
||||
@@ -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!))
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
})
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+1695
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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-'))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user