From c4264c9a01f77f94752c1fef450a31c0652460fc Mon Sep 17 00:00:00 2001 From: SIN CI Date: Sun, 26 Jul 2026 23:29:37 +0200 Subject: [PATCH] docs: preserve OpenCodex Fireworks provider research --- .../opencodex/fireworks-ai/GAP-ANALYSIS.md | 30 + .../fireworks-ai/LICENSE-OpenCodex-Apache-2.0 | 201 +++++++ .../opencodex/fireworks-ai/README.md | 24 + .../opencodex/fireworks-ai/SHA256SUMS | 6 + .../opencodex/fireworks-ai/SOURCE.json | 11 + .../fireworks-ai/fireworks-ai-provider.patch | 553 ++++++++++++++++++ .../fireworks-ai/source/fireworks_ai.rs | 392 +++++++++++++ 7 files changed, 1217 insertions(+) create mode 100644 docs/research/upstream-patches/opencodex/fireworks-ai/GAP-ANALYSIS.md create mode 100644 docs/research/upstream-patches/opencodex/fireworks-ai/LICENSE-OpenCodex-Apache-2.0 create mode 100644 docs/research/upstream-patches/opencodex/fireworks-ai/README.md create mode 100644 docs/research/upstream-patches/opencodex/fireworks-ai/SHA256SUMS create mode 100644 docs/research/upstream-patches/opencodex/fireworks-ai/SOURCE.json create mode 100644 docs/research/upstream-patches/opencodex/fireworks-ai/fireworks-ai-provider.patch create mode 100644 docs/research/upstream-patches/opencodex/fireworks-ai/source/fireworks_ai.rs diff --git a/docs/research/upstream-patches/opencodex/fireworks-ai/GAP-ANALYSIS.md b/docs/research/upstream-patches/opencodex/fireworks-ai/GAP-ANALYSIS.md new file mode 100644 index 00000000..31e67ba4 --- /dev/null +++ b/docs/research/upstream-patches/opencodex/fireworks-ai/GAP-ANALYSIS.md @@ -0,0 +1,30 @@ +# Fireworks provider gap analysis + +## Current SIN-Code strengths + +- Native Go profile and setup-wizard support. +- Curated multi-model Fireworks tournament pool. +- Explicit cost, context, vision and thinking metadata. +- Permission-gated tool surface. +- Optional SINator pool-router support. + +## Retained OpenCodex ideas not fully represented + +- Runtime discovery through `GET /v1/models`. +- ETag-aware remote catalog refresh. +- Local model-catalog cache. +- Five-second bounded refresh timeout. +- Merge/deduplication of returned models and known router fallbacks. +- Unit tests for alternate model response formats. + +## Recommended implementation boundary + +Do not port the Rust code directly. Implement a small Go catalog component +behind the existing Fireworks profile/fusion layer: + +- static curated lineup remains the safe fallback; +- dynamic discovery is optional and never blocks startup; +- cached data has an explicit age and provenance; +- capability overrides remain version-controlled; +- no API key, account data or live response is committed; +- tests use local fixtures only. diff --git a/docs/research/upstream-patches/opencodex/fireworks-ai/LICENSE-OpenCodex-Apache-2.0 b/docs/research/upstream-patches/opencodex/fireworks-ai/LICENSE-OpenCodex-Apache-2.0 new file mode 100644 index 00000000..4606e72e --- /dev/null +++ b/docs/research/upstream-patches/opencodex/fireworks-ai/LICENSE-OpenCodex-Apache-2.0 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2025 OpenAI + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/docs/research/upstream-patches/opencodex/fireworks-ai/README.md b/docs/research/upstream-patches/opencodex/fireworks-ai/README.md new file mode 100644 index 00000000..fa6d3226 --- /dev/null +++ b/docs/research/upstream-patches/opencodex/fireworks-ai/README.md @@ -0,0 +1,24 @@ +# OpenCodex Fireworks AI provider research + +**Status:** preserved research; not compiled into SIN-Code. + +This directory retains the only product-specific commit found in +`Delqhi/OpenCodex` before that fork was retired. The fork's `main` branch was +byte-for-byte aligned with its upstream at the time of the audit; the unique +work lived on `feat/fireworks-ai-provider`. + +SIN-Code already has a newer native Fireworks implementation. Therefore this +Rust provider is preserved as an attributable reference patch rather than +introduced as a second runtime implementation. + +## Ideas worth porting + +1. Refresh the Fireworks `/v1/models` catalog dynamically. +2. Cache the model catalog and apply a bounded network timeout. +3. Merge API results with router/model fallbacks that are absent from the API. +4. Normalize model capabilities such as context, vision and reasoning. +5. Add parser, deduplication, fallback and provider-construction tests. + +See `GAP-ANALYSIS.md` before porting. The exact original commit is retained in +`fireworks-ai-provider.patch`; the complete deleted repository is also kept as +a verified local Git bundle outside this repository. diff --git a/docs/research/upstream-patches/opencodex/fireworks-ai/SHA256SUMS b/docs/research/upstream-patches/opencodex/fireworks-ai/SHA256SUMS new file mode 100644 index 00000000..b66d9805 --- /dev/null +++ b/docs/research/upstream-patches/opencodex/fireworks-ai/SHA256SUMS @@ -0,0 +1,6 @@ +51619b6bd05ef0edcad9134aa354d874102a2b494f3f11a72d449e518124ca90 ./GAP-ANALYSIS.md +d17f227e4df5da1600391338865ce0f3055211760a36688f816941d58232d8dc ./LICENSE-OpenCodex-Apache-2.0 +6ee5cfc98cc8ce2802d5f801bafa9975b5594107d4d8b08673da22ffbbf8d8c5 ./README.md +4afa73e6c1c3880ac1877121dd707b934095dddcedd527c6645136ea40787762 ./SOURCE.json +33a38849b5ef59461f32a185491e24e0c57552b3d8e4910e7b87bb4c805829ba ./fireworks-ai-provider.patch +6c6f09922c921527f57bf654baf48fcacf94bd12ba2895eb938bbe60d2596db8 ./source/fireworks_ai.rs diff --git a/docs/research/upstream-patches/opencodex/fireworks-ai/SOURCE.json b/docs/research/upstream-patches/opencodex/fireworks-ai/SOURCE.json new file mode 100644 index 00000000..e1204c69 --- /dev/null +++ b/docs/research/upstream-patches/opencodex/fireworks-ai/SOURCE.json @@ -0,0 +1,11 @@ +{ + "source_repository": "Delqhi/OpenCodex", + "source_repository_default_head": "cd7835dfc275725b389f9c2575e0d33d21168f7c", + "source_branch": "feat/fireworks-ai-provider", + "source_commit": "4115d386d7e46245aa532cb4b419d8609b3969e2", + "source_commit_author": "ZOEsolar ", + "upstream_repository": "HackWidMaddy/OpenCodex", + "license": "Apache-2.0", + "retention_reason": "Only unique non-Dependabot product commit in the retired fork", + "runtime_status": "reference-only" +} diff --git a/docs/research/upstream-patches/opencodex/fireworks-ai/fireworks-ai-provider.patch b/docs/research/upstream-patches/opencodex/fireworks-ai/fireworks-ai-provider.patch new file mode 100644 index 00000000..97ffd837 --- /dev/null +++ b/docs/research/upstream-patches/opencodex/fireworks-ai/fireworks-ai-provider.patch @@ -0,0 +1,553 @@ +From 4115d386d7e46245aa532cb4b419d8609b3969e2 Mon Sep 17 00:00:00 2001 +From: ZOEsolar +Date: Mon, 25 May 2026 18:45:38 +0200 +Subject: [PATCH] feat: add Fireworks AI built-in model provider + +Fireworks AI provider plugin for OpenCodex. + +- New provider: FireworksAiModelProvider (based on NvidiaNimModelProvider) +- OpenAI-compatible Chat Completions API (wire_api: chat) +- Remote model catalog via /v1/models endpoint +- Hardcoded fallback for router models not in API response +- Supports 10+ models: kimi-k2p6, deepseek-v4-pro, glm-5p1, + qwen3p6-plus, minimax-m2p7, gpt-oss-120b, etc. +- Image input support via InputModality::Image +- Fast search instructions for shell work +- Full test suite (model parsing, capabilities, provider creation) +- Registered as built-in provider with config.toml support +--- + codex-rs/config/src/config_toml.rs | 4 +- + codex-rs/model-provider-info/src/lib.rs | 38 ++ + codex-rs/model-provider/src/fireworks_ai.rs | 392 ++++++++++++++++++++ + codex-rs/model-provider/src/lib.rs | 1 + + codex-rs/model-provider/src/provider.rs | 3 + + 5 files changed, 437 insertions(+), 1 deletion(-) + create mode 100644 codex-rs/model-provider/src/fireworks_ai.rs + +diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs +index 78a58ee..1649a6e 100644 +--- a/codex-rs/config/src/config_toml.rs ++++ b/codex-rs/config/src/config_toml.rs +@@ -35,6 +35,7 @@ use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID; + use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; + use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; + use codex_model_provider_info::ModelProviderInfo; ++use codex_model_provider_info::FIREWORKS_AI_PROVIDER_ID; + use codex_model_provider_info::NVIDIA_NIM_PROVIDER_ID; + use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; + use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; +@@ -61,8 +62,9 @@ use serde::Serialize; + use serde::de::Error as SerdeError; + use serde_json::Value as JsonValue; + +-const RESERVED_MODEL_PROVIDER_IDS: [&str; 5] = [ ++const RESERVED_MODEL_PROVIDER_IDS: [&str; 6] = [ + AMAZON_BEDROCK_PROVIDER_ID, ++ FIREWORKS_AI_PROVIDER_ID, + NVIDIA_NIM_PROVIDER_ID, + OPENAI_PROVIDER_ID, + OLLAMA_OSS_PROVIDER_ID, +diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs +index 720b62c..f0be84d 100644 +--- a/codex-rs/model-provider-info/src/lib.rs ++++ b/codex-rs/model-provider-info/src/lib.rs +@@ -47,6 +47,11 @@ pub const NVIDIA_NIM_PROVIDER_ID: &str = "nvidia-nim"; + pub const NVIDIA_NIM_DEFAULT_BASE_URL: &str = "https://integrate.api.nvidia.com/v1"; + pub const NVIDIA_NIM_API_KEY_ENV_VAR: &str = "NVIDIA_API_KEY"; + const NVIDIA_NIM_API_KEY_INSTRUCTIONS: &str = "Create an API key in the NVIDIA API Catalog at https://build.nvidia.com and set NVIDIA_API_KEY."; ++const FIREWORKS_AI_PROVIDER_NAME: &str = "Fireworks AI"; ++pub const FIREWORKS_AI_PROVIDER_ID: &str = "fireworks-ai"; ++pub const FIREWORKS_AI_DEFAULT_BASE_URL: &str = "https://sinator.delqhi.com/inference/v1"; ++pub const FIREWORKS_AI_API_KEY_ENV_VAR: &str = "FIREWORKS_AI_API_KEY"; ++const FIREWORKS_AI_API_KEY_INSTRUCTIONS: &str = "Set FIREWORKS_AI_API_KEY to your shared key."; + pub const LEGACY_OLLAMA_CHAT_PROVIDER_ID: &str = "ollama-chat"; + pub const OLLAMA_CHAT_PROVIDER_REMOVED_ERROR: &str = "`ollama-chat` is no longer supported.\nHow to fix: replace `ollama-chat` with `ollama` in `model_provider`, `oss_provider`, or `--local-provider`.\nMore info: https://github.com/openai/codex/discussions/7782"; + +@@ -431,6 +436,37 @@ impl ModelProviderInfo { + self.name == NVIDIA_NIM_PROVIDER_NAME + } + ++ pub fn create_fireworks_ai_provider() -> ModelProviderInfo { ++ let base_url = std::env::var("CODEX_FIREWORKS_AI_BASE_URL") ++ .ok() ++ .filter(|v| !v.trim().is_empty()) ++ .unwrap_or_else(|| FIREWORKS_AI_DEFAULT_BASE_URL.to_string()); ++ ++ ModelProviderInfo { ++ name: FIREWORKS_AI_PROVIDER_NAME.into(), ++ base_url: Some(base_url), ++ env_key: Some(FIREWORKS_AI_API_KEY_ENV_VAR.into()), ++ env_key_instructions: Some(FIREWORKS_AI_API_KEY_INSTRUCTIONS.into()), ++ experimental_bearer_token: None, ++ auth: None, ++ aws: None, ++ wire_api: WireApi::Chat, ++ query_params: None, ++ http_headers: None, ++ env_http_headers: None, ++ request_max_retries: None, ++ stream_max_retries: None, ++ stream_idle_timeout_ms: None, ++ websocket_connect_timeout_ms: None, ++ requires_openai_auth: false, ++ supports_websockets: false, ++ } ++ } ++ ++ pub fn is_fireworks_ai(&self) -> bool { ++ self.name == FIREWORKS_AI_PROVIDER_NAME ++ } ++ + pub fn supports_remote_compaction(&self) -> bool { + self.is_openai() || is_azure_responses_provider(&self.name, self.base_url.as_deref()) + } +@@ -454,6 +490,7 @@ pub fn built_in_model_providers( + let openai_provider = P::create_openai_provider(openai_base_url); + let amazon_bedrock_provider = P::create_amazon_bedrock_provider(/*aws*/ None); + let nvidia_nim_provider = P::create_nvidia_nim_provider(); ++ let fireworks_ai_provider = P::create_fireworks_ai_provider(); + + // Keep built-ins to first-party OpenAI integrations, explicitly supported + // partner endpoints, and local open source ("oss") providers. Users can add +@@ -462,6 +499,7 @@ pub fn built_in_model_providers( + (OPENAI_PROVIDER_ID, openai_provider), + (AMAZON_BEDROCK_PROVIDER_ID, amazon_bedrock_provider), + (NVIDIA_NIM_PROVIDER_ID, nvidia_nim_provider), ++ (FIREWORKS_AI_PROVIDER_ID, fireworks_ai_provider), + ( + OLLAMA_OSS_PROVIDER_ID, + create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses), +diff --git a/codex-rs/model-provider/src/fireworks_ai.rs b/codex-rs/model-provider/src/fireworks_ai.rs +new file mode 100644 +index 0000000..6b07508 +--- /dev/null ++++ b/codex-rs/model-provider/src/fireworks_ai.rs +@@ -0,0 +1,392 @@ ++use std::collections::HashSet; ++use std::path::PathBuf; ++use std::sync::Arc; ++use std::time::Duration; ++ ++use codex_api::ApiError; ++use codex_api::Provider; ++use codex_api::ReqwestTransport; ++use codex_api::SharedAuthProvider; ++use codex_api::TransportError; ++use codex_api::map_api_error; ++use codex_client::HttpTransport; ++use codex_login::AuthManager; ++use codex_login::CodexAuth; ++use codex_login::default_client::build_reqwest_client; ++use codex_model_provider_info::ModelProviderInfo; ++use codex_models_manager::manager::ModelsEndpointClient; ++use codex_models_manager::manager::OpenAiModelsManager; ++use codex_models_manager::manager::SharedModelsManager; ++use codex_models_manager::manager::StaticModelsManager; ++use codex_models_manager::model_info::BASE_INSTRUCTIONS; ++use codex_protocol::account::ProviderAccount; ++use codex_protocol::config_types::ReasoningSummary; ++use codex_protocol::error::CodexErr; ++use codex_protocol::error::Result; ++use codex_protocol::openai_models::ConfigShellToolType; ++use codex_protocol::openai_models::InputModality; ++use codex_protocol::openai_models::ModelInfo; ++use codex_protocol::openai_models::ModelVisibility; ++use codex_protocol::openai_models::ModelsResponse; ++use codex_protocol::openai_models::TruncationPolicyConfig; ++use codex_protocol::openai_models::WebSearchToolType; ++use http::Method; ++use http::header::ETAG; ++use serde::Deserialize; ++use tokio::time::timeout; ++ ++use crate::auth::auth_manager_for_provider; ++use crate::auth::resolve_provider_auth; ++use crate::provider::ModelProvider; ++use crate::provider::ProviderAccountResult; ++use crate::provider::ProviderAccountState; ++use crate::provider::ProviderCapabilities; ++ ++const MODELS_REFRESH_TIMEOUT: Duration = Duration::from_secs(5); ++const FIREWORKS_AI_MODELS_CACHE_FILE: &str = "fireworks_ai_models_cache.json"; ++const FIREWORKS_AI_DEFAULT_CONTEXT_WINDOW: i64 = 128_000; ++const FIREWORKS_AI_TOOL_OUTPUT_TOKEN_LIMIT: i64 = 10_000; ++const FIREWORKS_AI_BASE_INSTRUCTIONS_APPENDIX: &str = r#" ++ ++Performance note: prefer fast search tools such as `rg`, `rg --files`, and `git ls-files`."#; ++ ++/// Runtime provider for Fireworks AI OpenAI-compatible endpoints. ++#[derive(Clone, Debug)] ++pub(crate) struct FireworksAiModelProvider { ++ info: ModelProviderInfo, ++ auth_manager: Option>, ++} ++ ++impl FireworksAiModelProvider { ++ pub(crate) fn new( ++ provider_info: ModelProviderInfo, ++ _auth_manager: Option>, ++ ) -> Self { ++ let auth_manager = auth_manager_for_provider(/*auth_manager*/ None, &provider_info); ++ Self { ++ info: provider_info, ++ auth_manager, ++ } ++ } ++} ++ ++#[async_trait::async_trait] ++impl ModelProvider for FireworksAiModelProvider { ++ fn info(&self) -> &ModelProviderInfo { ++ &self.info ++ } ++ ++ fn capabilities(&self) -> ProviderCapabilities { ++ ProviderCapabilities { ++ namespace_tools: false, ++ image_generation: false, ++ web_search: false, ++ } ++ } ++ ++ fn auth_manager(&self) -> Option> { ++ self.auth_manager.clone() ++ } ++ ++ async fn auth(&self) -> Option { ++ match self.auth_manager.as_ref() { ++ Some(auth_manager) => auth_manager.auth().await, ++ None => None, ++ } ++ } ++ ++ fn account_state(&self) -> ProviderAccountResult { ++ let account = self ++ .info ++ .env_key ++ .as_deref() ++ .and_then(|env_key| std::env::var(env_key).ok()) ++ .filter(|api_key| !api_key.trim().is_empty()) ++ .map(|_| ProviderAccount::ApiKey); ++ ++ Ok(ProviderAccountState { ++ account, ++ requires_openai_auth: false, ++ }) ++ } ++ ++ async fn api_provider(&self) -> Result { ++ let auth = self.auth().await; ++ self.info() ++ .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode)) ++ } ++ ++ async fn api_auth(&self) -> Result { ++ let auth = self.auth().await; ++ resolve_provider_auth(auth.as_ref(), self.info()) ++ } ++ ++ fn models_manager( ++ &self, ++ codex_home: PathBuf, ++ config_model_catalog: Option, ++ ) -> SharedModelsManager { ++ match config_model_catalog { ++ Some(model_catalog) => Arc::new(StaticModelsManager::new( ++ self.auth_manager.clone(), ++ model_catalog, ++ )), ++ None => { ++ let endpoint = Arc::new(FireworksAiModelsEndpoint::new( ++ self.info.clone(), ++ self.auth_manager.clone(), ++ )); ++ Arc::new(OpenAiModelsManager::new_with_base_catalog( ++ codex_home, ++ endpoint, ++ self.auth_manager.clone(), ++ ModelsResponse::default(), ++ FIREWORKS_AI_MODELS_CACHE_FILE, ++ /*use_remote_models_only*/ true, ++ )) ++ } ++ } ++ } ++} ++ ++#[derive(Debug)] ++struct FireworksAiModelsEndpoint { ++ provider_info: ModelProviderInfo, ++ auth_manager: Option>, ++} ++ ++impl FireworksAiModelsEndpoint { ++ fn new(provider_info: ModelProviderInfo, auth_manager: Option>) -> Self { ++ Self { ++ provider_info, ++ auth_manager, ++ } ++ } ++ ++ async fn auth(&self) -> Option { ++ match self.auth_manager.as_ref() { ++ Some(auth_manager) => auth_manager.auth().await, ++ None => None, ++ } ++ } ++} ++ ++#[async_trait::async_trait] ++impl ModelsEndpointClient for FireworksAiModelsEndpoint { ++ fn has_command_auth(&self) -> bool { ++ self.provider_info.has_command_auth() ++ } ++ ++ fn supports_remote_model_catalog(&self) -> bool { ++ true ++ } ++ ++ async fn uses_codex_backend(&self) -> bool { ++ false ++ } ++ ++ async fn list_models(&self, _client_version: &str) -> Result<(Vec, Option)> { ++ let auth = self.auth().await; ++ let auth_mode = auth.as_ref().map(CodexAuth::auth_mode); ++ let api_provider = self.provider_info.to_api_provider(auth_mode)?; ++ let api_auth = resolve_provider_auth(auth.as_ref(), &self.provider_info)?; ++ ++ let request = api_provider.build_request(Method::GET, "models"); ++ let request = api_auth ++ .apply_auth(request) ++ .await ++ .map_err(TransportError::from) ++ .map_err(ApiError::Transport) ++ .map_err(map_api_error)?; ++ ++ let transport = ReqwestTransport::new(build_reqwest_client()); ++ let response = timeout(MODELS_REFRESH_TIMEOUT, transport.execute(request)) ++ .await ++ .map_err(|_| CodexErr::Timeout)? ++ .map_err(ApiError::Transport) ++ .map_err(map_api_error)?; ++ let etag = response ++ .headers ++ .get(ETAG) ++ .and_then(|value| value.to_str().ok()) ++ .map(ToString::to_string); ++ ++ Ok((parse_fireworks_ai_models(&response.body)?, etag)) ++ } ++} ++ ++#[derive(Debug, Deserialize)] ++struct OpenAiModelListResponse { ++ data: Vec, ++} ++ ++#[derive(Debug, Deserialize)] ++struct OpenAiModel { ++ id: String, ++ owned_by: Option, ++} ++ ++/// Models known to work but not returned by /v1/models (routers, etc.) ++const FIREWORKS_AI_HARDCODED_MODELS: &[&str] = &[ ++ "accounts/fireworks/routers/glm-5p1-fast", ++ "accounts/fireworks/routers/kimi-k2p6-turbo", ++ "accounts/fireworks/models/qwen3p6-plus", ++ "accounts/fireworks/models/minimax-m2p7", ++ "accounts/fireworks/models/minimax-m2p5", ++]; ++ ++fn parse_fireworks_ai_models(body: &[u8]) -> Result> { ++ if let Ok(ModelsResponse { models }) = serde_json::from_slice::(body) { ++ return Ok(models); ++ } ++ ++ let OpenAiModelListResponse { data } = serde_json::from_slice(body)?; ++ let mut seen = HashSet::new(); ++ ++ // Always include hardcoded models ++ for slug in FIREWORKS_AI_HARDCODED_MODELS { ++ seen.insert(slug.to_string()); ++ } ++ ++ let mut models = Vec::new(); ++ for model in data { ++ let id = model.id.trim(); ++ if id.is_empty() || !seen.insert(id.to_string()) { ++ continue; ++ } ++ let priority = i32::try_from(models.len()).unwrap_or(i32::MAX); ++ models.push(fireworks_ai_model_info( ++ id, ++ model.owned_by.as_deref(), ++ priority, ++ )); ++ } ++ ++ // Add hardcoded models at the end (lower priority) ++ for slug in FIREWORKS_AI_HARDCODED_MODELS { ++ let priority = i32::try_from(models.len()).unwrap_or(i32::MAX); ++ models.push(fireworks_ai_model_info(slug, Some("fireworks"), priority)); ++ } ++ Ok(models) ++} ++ ++fn fireworks_ai_model_info(slug: &str, owned_by: Option<&str>, priority: i32) -> ModelInfo { ++ let description = owned_by ++ .filter(|owner| !owner.trim().is_empty()) ++ .map(|owner| format!("Fireworks AI model owned by {owner}")) ++ .unwrap_or_else(|| "Fireworks AI model".to_string()); ++ ++ ModelInfo { ++ slug: slug.to_string(), ++ display_name: slug.to_string(), ++ description: Some(description), ++ default_reasoning_level: None, ++ supported_reasoning_levels: Vec::new(), ++ shell_type: ConfigShellToolType::ShellCommand, ++ visibility: ModelVisibility::List, ++ supported_in_api: true, ++ priority, ++ additional_speed_tiers: Vec::new(), ++ service_tiers: Vec::new(), ++ availability_nux: None, ++ upgrade: None, ++ base_instructions: format!("{BASE_INSTRUCTIONS}{FIREWORKS_AI_BASE_INSTRUCTIONS_APPENDIX}"), ++ model_messages: None, ++ supports_reasoning_summaries: false, ++ default_reasoning_summary: ReasoningSummary::Auto, ++ support_verbosity: false, ++ default_verbosity: None, ++ apply_patch_tool_type: None, ++ web_search_tool_type: WebSearchToolType::Text, ++ truncation_policy: TruncationPolicyConfig::tokens(FIREWORKS_AI_TOOL_OUTPUT_TOKEN_LIMIT), ++ supports_parallel_tool_calls: false, ++ supports_image_detail_original: false, ++ context_window: Some(FIREWORKS_AI_DEFAULT_CONTEXT_WINDOW), ++ max_context_window: Some(FIREWORKS_AI_DEFAULT_CONTEXT_WINDOW), ++ auto_compact_token_limit: None, ++ effective_context_window_percent: 95, ++ experimental_supported_tools: Vec::new(), ++ input_modalities: vec![InputModality::Text, InputModality::Image], ++ used_fallback_model_metadata: false, ++ supports_search_tool: false, ++ } ++} ++ ++#[cfg(test)] ++mod tests { ++ use codex_models_manager::manager::RefreshStrategy; ++ use pretty_assertions::assert_eq; ++ use serde_json::json; ++ use wiremock::Mock; ++ use wiremock::MockServer; ++ use wiremock::ResponseTemplate; ++ use wiremock::matchers::method; ++ use wiremock::matchers::path; ++ ++ use super::*; ++ ++ #[test] ++ fn parses_standard_openai_models_response() { ++ let body = br#"{ ++ "object": "list", ++ "data": [ ++ {"id": "accounts/fireworks/models/kimi-k2p6", "object": "model", "owned_by": "fireworks"}, ++ {"id": "accounts/fireworks/routers/glm-5p1-fast", "object": "model", "owned_by": "fireworks"} ++ ] ++ }"#; ++ ++ let models = parse_fireworks_ai_models(body).expect("models should parse"); ++ ++ assert_eq!(models.len(), 2); ++ assert_eq!(models[0].slug, "accounts/fireworks/models/kimi-k2p6"); ++ assert_eq!(models[0].visibility, ModelVisibility::List); ++ assert_eq!(models[0].shell_type, ConfigShellToolType::ShellCommand); ++ assert!(!models[0].supports_parallel_tool_calls); ++ } ++ ++ #[test] ++ fn provider_capabilities_disable_openai_hosted_features() { ++ let provider = FireworksAiModelProvider::new( ++ ModelProviderInfo::create_fireworks_ai_provider(), ++ /*auth_manager*/ None, ++ ); ++ ++ assert_eq!( ++ provider.capabilities(), ++ ProviderCapabilities { ++ namespace_tools: false, ++ image_generation: false, ++ web_search: false, ++ } ++ ); ++ } ++ ++ #[tokio::test] ++ async fn models_manager_fetches_standard_openai_models_endpoint() { ++ let server = MockServer::start().await; ++ Mock::given(method("GET")) ++ .and(path("/v1/models")) ++ .respond_with(ResponseTemplate::new(200).set_body_json(json!({ ++ "object": "list", ++ "data": [ ++ {"id": "accounts/fireworks/models/deepseek-v4-pro", "object": "model", "owned_by": "fireworks"} ++ ] ++ }))) ++ .mount(&server) ++ .await; ++ ++ let mut provider_info = ModelProviderInfo::create_fireworks_ai_provider(); ++ provider_info.base_url = Some(format!("{}/v1", server.uri())); ++ provider_info.env_key = None; ++ let provider = FireworksAiModelProvider::new(provider_info, /*auth_manager*/ None); ++ let manager = provider.models_manager( ++ std::env::temp_dir().join(format!("codex-fireworks-ai-test-{}", std::process::id())), ++ /*config_model_catalog*/ None, ++ ); ++ ++ let catalog = manager.raw_model_catalog(RefreshStrategy::Online).await; ++ ++ assert_eq!(catalog.models.len(), 1); ++ assert_eq!(catalog.models[0].slug, "accounts/fireworks/models/deepseek-v4-pro"); ++ } ++} +diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs +index 0505d50..d31f5f3 100644 +--- a/codex-rs/model-provider/src/lib.rs ++++ b/codex-rs/model-provider/src/lib.rs +@@ -2,6 +2,7 @@ mod amazon_bedrock; + mod auth; + mod bearer_auth_provider; + mod models_endpoint; ++mod fireworks_ai; + mod nvidia_nim; + mod provider; + +diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs +index 5c1fcda..ceea391 100644 +--- a/codex-rs/model-provider/src/provider.rs ++++ b/codex-rs/model-provider/src/provider.rs +@@ -17,6 +17,7 @@ use crate::amazon_bedrock::AmazonBedrockModelProvider; + use crate::auth::auth_manager_for_provider; + use crate::auth::resolve_provider_auth; + use crate::models_endpoint::OpenAiModelsEndpoint; ++use crate::fireworks_ai::FireworksAiModelProvider; + use crate::nvidia_nim::NvidiaNimModelProvider; + + /// Optional provider-backed features that Codex may expose at runtime. +@@ -154,6 +155,8 @@ pub fn create_model_provider( + Arc::new(AmazonBedrockModelProvider::new(provider_info)) + } else if provider_info.is_nvidia_nim() { + Arc::new(NvidiaNimModelProvider::new(provider_info, auth_manager)) ++ } else if provider_info.is_fireworks_ai() { ++ Arc::new(FireworksAiModelProvider::new(provider_info, auth_manager)) + } else { + Arc::new(ConfiguredModelProvider::new(provider_info, auth_manager)) + } +-- +2.50.1 (Apple Git-155) + diff --git a/docs/research/upstream-patches/opencodex/fireworks-ai/source/fireworks_ai.rs b/docs/research/upstream-patches/opencodex/fireworks-ai/source/fireworks_ai.rs new file mode 100644 index 00000000..6b07508f --- /dev/null +++ b/docs/research/upstream-patches/opencodex/fireworks-ai/source/fireworks_ai.rs @@ -0,0 +1,392 @@ +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use codex_api::ApiError; +use codex_api::Provider; +use codex_api::ReqwestTransport; +use codex_api::SharedAuthProvider; +use codex_api::TransportError; +use codex_api::map_api_error; +use codex_client::HttpTransport; +use codex_login::AuthManager; +use codex_login::CodexAuth; +use codex_login::default_client::build_reqwest_client; +use codex_model_provider_info::ModelProviderInfo; +use codex_models_manager::manager::ModelsEndpointClient; +use codex_models_manager::manager::OpenAiModelsManager; +use codex_models_manager::manager::SharedModelsManager; +use codex_models_manager::manager::StaticModelsManager; +use codex_models_manager::model_info::BASE_INSTRUCTIONS; +use codex_protocol::account::ProviderAccount; +use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::error::CodexErr; +use codex_protocol::error::Result; +use codex_protocol::openai_models::ConfigShellToolType; +use codex_protocol::openai_models::InputModality; +use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ModelVisibility; +use codex_protocol::openai_models::ModelsResponse; +use codex_protocol::openai_models::TruncationPolicyConfig; +use codex_protocol::openai_models::WebSearchToolType; +use http::Method; +use http::header::ETAG; +use serde::Deserialize; +use tokio::time::timeout; + +use crate::auth::auth_manager_for_provider; +use crate::auth::resolve_provider_auth; +use crate::provider::ModelProvider; +use crate::provider::ProviderAccountResult; +use crate::provider::ProviderAccountState; +use crate::provider::ProviderCapabilities; + +const MODELS_REFRESH_TIMEOUT: Duration = Duration::from_secs(5); +const FIREWORKS_AI_MODELS_CACHE_FILE: &str = "fireworks_ai_models_cache.json"; +const FIREWORKS_AI_DEFAULT_CONTEXT_WINDOW: i64 = 128_000; +const FIREWORKS_AI_TOOL_OUTPUT_TOKEN_LIMIT: i64 = 10_000; +const FIREWORKS_AI_BASE_INSTRUCTIONS_APPENDIX: &str = r#" + +Performance note: prefer fast search tools such as `rg`, `rg --files`, and `git ls-files`."#; + +/// Runtime provider for Fireworks AI OpenAI-compatible endpoints. +#[derive(Clone, Debug)] +pub(crate) struct FireworksAiModelProvider { + info: ModelProviderInfo, + auth_manager: Option>, +} + +impl FireworksAiModelProvider { + pub(crate) fn new( + provider_info: ModelProviderInfo, + _auth_manager: Option>, + ) -> Self { + let auth_manager = auth_manager_for_provider(/*auth_manager*/ None, &provider_info); + Self { + info: provider_info, + auth_manager, + } + } +} + +#[async_trait::async_trait] +impl ModelProvider for FireworksAiModelProvider { + fn info(&self) -> &ModelProviderInfo { + &self.info + } + + fn capabilities(&self) -> ProviderCapabilities { + ProviderCapabilities { + namespace_tools: false, + image_generation: false, + web_search: false, + } + } + + fn auth_manager(&self) -> Option> { + self.auth_manager.clone() + } + + async fn auth(&self) -> Option { + match self.auth_manager.as_ref() { + Some(auth_manager) => auth_manager.auth().await, + None => None, + } + } + + fn account_state(&self) -> ProviderAccountResult { + let account = self + .info + .env_key + .as_deref() + .and_then(|env_key| std::env::var(env_key).ok()) + .filter(|api_key| !api_key.trim().is_empty()) + .map(|_| ProviderAccount::ApiKey); + + Ok(ProviderAccountState { + account, + requires_openai_auth: false, + }) + } + + async fn api_provider(&self) -> Result { + let auth = self.auth().await; + self.info() + .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode)) + } + + async fn api_auth(&self) -> Result { + let auth = self.auth().await; + resolve_provider_auth(auth.as_ref(), self.info()) + } + + fn models_manager( + &self, + codex_home: PathBuf, + config_model_catalog: Option, + ) -> SharedModelsManager { + match config_model_catalog { + Some(model_catalog) => Arc::new(StaticModelsManager::new( + self.auth_manager.clone(), + model_catalog, + )), + None => { + let endpoint = Arc::new(FireworksAiModelsEndpoint::new( + self.info.clone(), + self.auth_manager.clone(), + )); + Arc::new(OpenAiModelsManager::new_with_base_catalog( + codex_home, + endpoint, + self.auth_manager.clone(), + ModelsResponse::default(), + FIREWORKS_AI_MODELS_CACHE_FILE, + /*use_remote_models_only*/ true, + )) + } + } + } +} + +#[derive(Debug)] +struct FireworksAiModelsEndpoint { + provider_info: ModelProviderInfo, + auth_manager: Option>, +} + +impl FireworksAiModelsEndpoint { + fn new(provider_info: ModelProviderInfo, auth_manager: Option>) -> Self { + Self { + provider_info, + auth_manager, + } + } + + async fn auth(&self) -> Option { + match self.auth_manager.as_ref() { + Some(auth_manager) => auth_manager.auth().await, + None => None, + } + } +} + +#[async_trait::async_trait] +impl ModelsEndpointClient for FireworksAiModelsEndpoint { + fn has_command_auth(&self) -> bool { + self.provider_info.has_command_auth() + } + + fn supports_remote_model_catalog(&self) -> bool { + true + } + + async fn uses_codex_backend(&self) -> bool { + false + } + + async fn list_models(&self, _client_version: &str) -> Result<(Vec, Option)> { + let auth = self.auth().await; + let auth_mode = auth.as_ref().map(CodexAuth::auth_mode); + let api_provider = self.provider_info.to_api_provider(auth_mode)?; + let api_auth = resolve_provider_auth(auth.as_ref(), &self.provider_info)?; + + let request = api_provider.build_request(Method::GET, "models"); + let request = api_auth + .apply_auth(request) + .await + .map_err(TransportError::from) + .map_err(ApiError::Transport) + .map_err(map_api_error)?; + + let transport = ReqwestTransport::new(build_reqwest_client()); + let response = timeout(MODELS_REFRESH_TIMEOUT, transport.execute(request)) + .await + .map_err(|_| CodexErr::Timeout)? + .map_err(ApiError::Transport) + .map_err(map_api_error)?; + let etag = response + .headers + .get(ETAG) + .and_then(|value| value.to_str().ok()) + .map(ToString::to_string); + + Ok((parse_fireworks_ai_models(&response.body)?, etag)) + } +} + +#[derive(Debug, Deserialize)] +struct OpenAiModelListResponse { + data: Vec, +} + +#[derive(Debug, Deserialize)] +struct OpenAiModel { + id: String, + owned_by: Option, +} + +/// Models known to work but not returned by /v1/models (routers, etc.) +const FIREWORKS_AI_HARDCODED_MODELS: &[&str] = &[ + "accounts/fireworks/routers/glm-5p1-fast", + "accounts/fireworks/routers/kimi-k2p6-turbo", + "accounts/fireworks/models/qwen3p6-plus", + "accounts/fireworks/models/minimax-m2p7", + "accounts/fireworks/models/minimax-m2p5", +]; + +fn parse_fireworks_ai_models(body: &[u8]) -> Result> { + if let Ok(ModelsResponse { models }) = serde_json::from_slice::(body) { + return Ok(models); + } + + let OpenAiModelListResponse { data } = serde_json::from_slice(body)?; + let mut seen = HashSet::new(); + + // Always include hardcoded models + for slug in FIREWORKS_AI_HARDCODED_MODELS { + seen.insert(slug.to_string()); + } + + let mut models = Vec::new(); + for model in data { + let id = model.id.trim(); + if id.is_empty() || !seen.insert(id.to_string()) { + continue; + } + let priority = i32::try_from(models.len()).unwrap_or(i32::MAX); + models.push(fireworks_ai_model_info( + id, + model.owned_by.as_deref(), + priority, + )); + } + + // Add hardcoded models at the end (lower priority) + for slug in FIREWORKS_AI_HARDCODED_MODELS { + let priority = i32::try_from(models.len()).unwrap_or(i32::MAX); + models.push(fireworks_ai_model_info(slug, Some("fireworks"), priority)); + } + Ok(models) +} + +fn fireworks_ai_model_info(slug: &str, owned_by: Option<&str>, priority: i32) -> ModelInfo { + let description = owned_by + .filter(|owner| !owner.trim().is_empty()) + .map(|owner| format!("Fireworks AI model owned by {owner}")) + .unwrap_or_else(|| "Fireworks AI model".to_string()); + + ModelInfo { + slug: slug.to_string(), + display_name: slug.to_string(), + description: Some(description), + default_reasoning_level: None, + supported_reasoning_levels: Vec::new(), + shell_type: ConfigShellToolType::ShellCommand, + visibility: ModelVisibility::List, + supported_in_api: true, + priority, + additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), + availability_nux: None, + upgrade: None, + base_instructions: format!("{BASE_INSTRUCTIONS}{FIREWORKS_AI_BASE_INSTRUCTIONS_APPENDIX}"), + model_messages: None, + supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, + support_verbosity: false, + default_verbosity: None, + apply_patch_tool_type: None, + web_search_tool_type: WebSearchToolType::Text, + truncation_policy: TruncationPolicyConfig::tokens(FIREWORKS_AI_TOOL_OUTPUT_TOKEN_LIMIT), + supports_parallel_tool_calls: false, + supports_image_detail_original: false, + context_window: Some(FIREWORKS_AI_DEFAULT_CONTEXT_WINDOW), + max_context_window: Some(FIREWORKS_AI_DEFAULT_CONTEXT_WINDOW), + auto_compact_token_limit: None, + effective_context_window_percent: 95, + experimental_supported_tools: Vec::new(), + input_modalities: vec![InputModality::Text, InputModality::Image], + used_fallback_model_metadata: false, + supports_search_tool: false, + } +} + +#[cfg(test)] +mod tests { + use codex_models_manager::manager::RefreshStrategy; + use pretty_assertions::assert_eq; + use serde_json::json; + use wiremock::Mock; + use wiremock::MockServer; + use wiremock::ResponseTemplate; + use wiremock::matchers::method; + use wiremock::matchers::path; + + use super::*; + + #[test] + fn parses_standard_openai_models_response() { + let body = br#"{ + "object": "list", + "data": [ + {"id": "accounts/fireworks/models/kimi-k2p6", "object": "model", "owned_by": "fireworks"}, + {"id": "accounts/fireworks/routers/glm-5p1-fast", "object": "model", "owned_by": "fireworks"} + ] + }"#; + + let models = parse_fireworks_ai_models(body).expect("models should parse"); + + assert_eq!(models.len(), 2); + assert_eq!(models[0].slug, "accounts/fireworks/models/kimi-k2p6"); + assert_eq!(models[0].visibility, ModelVisibility::List); + assert_eq!(models[0].shell_type, ConfigShellToolType::ShellCommand); + assert!(!models[0].supports_parallel_tool_calls); + } + + #[test] + fn provider_capabilities_disable_openai_hosted_features() { + let provider = FireworksAiModelProvider::new( + ModelProviderInfo::create_fireworks_ai_provider(), + /*auth_manager*/ None, + ); + + assert_eq!( + provider.capabilities(), + ProviderCapabilities { + namespace_tools: false, + image_generation: false, + web_search: false, + } + ); + } + + #[tokio::test] + async fn models_manager_fetches_standard_openai_models_endpoint() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/v1/models")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "object": "list", + "data": [ + {"id": "accounts/fireworks/models/deepseek-v4-pro", "object": "model", "owned_by": "fireworks"} + ] + }))) + .mount(&server) + .await; + + let mut provider_info = ModelProviderInfo::create_fireworks_ai_provider(); + provider_info.base_url = Some(format!("{}/v1", server.uri())); + provider_info.env_key = None; + let provider = FireworksAiModelProvider::new(provider_info, /*auth_manager*/ None); + let manager = provider.models_manager( + std::env::temp_dir().join(format!("codex-fireworks-ai-test-{}", std::process::id())), + /*config_model_catalog*/ None, + ); + + let catalog = manager.raw_model_catalog(RefreshStrategy::Online).await; + + assert_eq!(catalog.models.len(), 1); + assert_eq!(catalog.models[0].slug, "accounts/fireworks/models/deepseek-v4-pro"); + } +}