mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-06 22:01:44 +08:00
feat: Add Codex OAuth FAST mode toggle (#2210)
* Add Codex OAuth FAST mode toggle * fix(codex-oauth): default FAST mode to off to avoid surprise quota burn service_tier="priority" consumes ChatGPT subscription quota at a higher rate. Users must now opt in explicitly rather than inherit FAST mode silently when this feature ships. --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -262,6 +262,9 @@ pub struct ProviderMeta {
|
||||
/// conversions fall back to provider ID.
|
||||
#[serde(rename = "promptCacheKey", skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_cache_key: Option<String>,
|
||||
/// Codex OAuth FAST mode: inject `service_tier = "priority"` for ChatGPT Codex requests.
|
||||
#[serde(rename = "codexFastMode", skip_serializing_if = "Option::is_none")]
|
||||
pub codex_fast_mode: Option<bool>,
|
||||
/// 累加模式应用中,该 provider 是否已写入 live config。
|
||||
/// `None` 表示旧数据/未知状态,`Some(false)` 表示明确仅存在于数据库中。
|
||||
#[serde(rename = "liveConfigManaged", skip_serializing_if = "Option::is_none")]
|
||||
@@ -277,6 +280,12 @@ pub struct ProviderMeta {
|
||||
}
|
||||
|
||||
impl ProviderMeta {
|
||||
/// Codex OAuth FAST mode 是否启用。默认关闭,因为 `service_tier="priority"`
|
||||
/// 会按更高速率消耗 ChatGPT 订阅配额,用户需显式开启以换取更低延迟。
|
||||
pub fn codex_fast_mode_enabled(&self) -> bool {
|
||||
self.codex_fast_mode.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 解析指定托管认证供应商绑定的账号 ID。
|
||||
///
|
||||
/// 新版优先读取 authBinding,旧版继续兼容 githubAccountId。
|
||||
|
||||
@@ -151,10 +151,16 @@ pub fn transform_claude_request_for_api_format(
|
||||
"openai_responses" => {
|
||||
// Codex OAuth (ChatGPT Plus/Pro 反代) 需要在请求体里强制 store: false
|
||||
// + include: ["reasoning.encrypted_content"],由 transform 层统一处理。
|
||||
let codex_fast_mode = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.map(|m| m.codex_fast_mode_enabled())
|
||||
.unwrap_or(false);
|
||||
super::transform_responses::anthropic_to_responses(
|
||||
body,
|
||||
Some(cache_key),
|
||||
is_codex_oauth,
|
||||
codex_fast_mode,
|
||||
)
|
||||
}
|
||||
"openai_chat" => {
|
||||
@@ -1330,6 +1336,43 @@ mod tests {
|
||||
assert_eq!(transformed["prompt_cache_key"], "explicit-cache-key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_claude_request_for_api_format_codex_oauth_fast_mode_off() {
|
||||
let provider = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex"
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
provider_type: Some("codex_oauth".to_string()),
|
||||
codex_fast_mode: Some(false),
|
||||
..ProviderMeta::default()
|
||||
},
|
||||
);
|
||||
let body = json!({
|
||||
"model": "gpt-5.4",
|
||||
"messages": [{ "role": "user", "content": "hello" }],
|
||||
"max_tokens": 128
|
||||
});
|
||||
|
||||
let transformed = transform_claude_request_for_api_format(
|
||||
body,
|
||||
&provider,
|
||||
"openai_responses",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(transformed["store"], json!(false));
|
||||
assert!(transformed.get("service_tier").is_none());
|
||||
assert_eq!(
|
||||
transformed["include"],
|
||||
json!(["reasoning.encrypted_content"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_claude_request_for_api_format_gemini_native() {
|
||||
let provider = create_provider_with_meta(
|
||||
|
||||
@@ -17,10 +17,13 @@ use serde_json::{json, Value};
|
||||
/// `is_codex_oauth`: 当目标后端是 ChatGPT Plus/Pro 反代 (`chatgpt.com/backend-api/codex`) 时为 true。
|
||||
/// 该后端强制要求 `store: false`,并要求 `include` 包含 `reasoning.encrypted_content`
|
||||
/// 以便在无服务端状态下保持多轮 reasoning 上下文。
|
||||
/// `codex_fast_mode`: 仅在 `is_codex_oauth` 为 true 时生效,控制是否注入
|
||||
/// `service_tier = "priority"`。
|
||||
pub fn anthropic_to_responses(
|
||||
body: Value,
|
||||
cache_key: Option<&str>,
|
||||
is_codex_oauth: bool,
|
||||
codex_fast_mode: bool,
|
||||
) -> Result<Value, ProxyError> {
|
||||
let mut result = json!({});
|
||||
|
||||
@@ -125,10 +128,15 @@ pub fn anthropic_to_responses(
|
||||
// (codex-rs 结构体根本没有这三个字段,OpenAI 自己的客户端不发它们)
|
||||
// - instructions / tools / parallel_tool_calls: 必填字段,缺则兜底默认值
|
||||
// (cc-switch 的 transform 当前是"条件写入",可能产生缺失)
|
||||
// - service_tier: 仅在 FAST mode 开启时写入 "priority"
|
||||
// (与 OpenAI 官方 codex-rs 当前请求结构保持一致)
|
||||
// - stream: 必须永远 true(codex-rs 硬编码 true,且 cc-switch 的
|
||||
// SSE 解析层只处理流式响应,强制覆盖避免客户端误传 false)
|
||||
if is_codex_oauth {
|
||||
result["store"] = json!(false);
|
||||
if codex_fast_mode {
|
||||
result["service_tier"] = json!("priority");
|
||||
}
|
||||
|
||||
const REASONING_MARKER: &str = "reasoning.encrypted_content";
|
||||
let mut includes: Vec<Value> = body
|
||||
@@ -520,7 +528,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["model"], "gpt-4o");
|
||||
assert_eq!(result["max_output_tokens"], 1024);
|
||||
assert_eq!(result["input"][0]["role"], "user");
|
||||
@@ -539,7 +547,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["instructions"], "You are a helpful assistant.");
|
||||
// system should not appear in input
|
||||
assert_eq!(result["input"].as_array().unwrap().len(), 1);
|
||||
@@ -557,7 +565,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["instructions"], "Part 1\n\nPart 2");
|
||||
}
|
||||
|
||||
@@ -574,7 +582,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["tools"][0]["type"], "function");
|
||||
assert_eq!(result["tools"][0]["name"], "get_weather");
|
||||
assert!(result["tools"][0].get("parameters").is_some());
|
||||
@@ -591,7 +599,7 @@ mod tests {
|
||||
"tool_choice": {"type": "any"}
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["tool_choice"], "required");
|
||||
}
|
||||
|
||||
@@ -604,7 +612,7 @@ mod tests {
|
||||
"tool_choice": {"type": "tool", "name": "get_weather"}
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["tool_choice"]["type"], "function");
|
||||
assert_eq!(result["tool_choice"]["name"], "get_weather");
|
||||
}
|
||||
@@ -623,7 +631,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
let input_arr = result["input"].as_array().unwrap();
|
||||
|
||||
// Should produce: assistant message (text) + function_call item
|
||||
@@ -653,7 +661,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
let input_arr = result["input"].as_array().unwrap();
|
||||
|
||||
// Should produce: function_call_output item (lifted)
|
||||
@@ -677,7 +685,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
let input_arr = result["input"].as_array().unwrap();
|
||||
|
||||
// thinking should be discarded, only text remains
|
||||
@@ -700,7 +708,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
let content = result["input"][0]["content"].as_array().unwrap();
|
||||
|
||||
assert_eq!(content[0]["type"], "input_text");
|
||||
@@ -858,7 +866,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["model"], "o3-mini");
|
||||
}
|
||||
|
||||
@@ -870,7 +878,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, Some("my-provider-id"), false).unwrap();
|
||||
let result = anthropic_to_responses(input, Some("my-provider-id"), false, true).unwrap();
|
||||
assert_eq!(result["prompt_cache_key"], "my-provider-id");
|
||||
}
|
||||
|
||||
@@ -888,7 +896,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert!(result["tools"][0].get("cache_control").is_none());
|
||||
}
|
||||
|
||||
@@ -905,7 +913,7 @@ mod tests {
|
||||
}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert!(result["input"][0]["content"][0]
|
||||
.get("cache_control")
|
||||
.is_none());
|
||||
@@ -967,7 +975,7 @@ mod tests {
|
||||
"max_tokens": 4096,
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["max_output_tokens"], 4096);
|
||||
assert!(result.get("max_completion_tokens").is_none());
|
||||
}
|
||||
@@ -981,7 +989,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["reasoning"]["effort"], "xhigh");
|
||||
}
|
||||
|
||||
@@ -995,7 +1003,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["reasoning"]["effort"], "low");
|
||||
}
|
||||
|
||||
@@ -1008,7 +1016,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["reasoning"]["effort"], "low");
|
||||
}
|
||||
|
||||
@@ -1021,7 +1029,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["reasoning"]["effort"], "medium");
|
||||
}
|
||||
|
||||
@@ -1034,7 +1042,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["reasoning"]["effort"], "high");
|
||||
}
|
||||
|
||||
@@ -1047,7 +1055,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert_eq!(result["reasoning"]["effort"], "xhigh");
|
||||
}
|
||||
|
||||
@@ -1060,7 +1068,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
assert!(result.get("reasoning").is_none());
|
||||
}
|
||||
|
||||
@@ -1074,10 +1082,11 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true).unwrap();
|
||||
let result = anthropic_to_responses(input, None, true, true).unwrap();
|
||||
|
||||
// store 必须显式为 false(ChatGPT 后端拒绝 true)
|
||||
assert_eq!(result["store"], json!(false));
|
||||
assert_eq!(result["service_tier"], json!("priority"));
|
||||
|
||||
// include 必须包含 reasoning.encrypted_content(无服务端状态下保持多轮 reasoning)
|
||||
assert_eq!(result["include"], json!(["reasoning.encrypted_content"]));
|
||||
@@ -1093,9 +1102,10 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
|
||||
assert!(result.get("store").is_none());
|
||||
assert!(result.get("service_tier").is_none());
|
||||
assert!(result.get("include").is_none());
|
||||
}
|
||||
|
||||
@@ -1109,7 +1119,7 @@ mod tests {
|
||||
"include": ["something.else", "reasoning.encrypted_content"]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true).unwrap();
|
||||
let result = anthropic_to_responses(input, None, true, true).unwrap();
|
||||
let includes = result["include"]
|
||||
.as_array()
|
||||
.expect("include should be array");
|
||||
@@ -1130,6 +1140,21 @@ mod tests {
|
||||
assert_eq!(marker_count, 1, "marker 不应被重复添加(idempotent 失败)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_to_responses_codex_oauth_fast_mode_can_be_disabled() {
|
||||
let input = json!({
|
||||
"model": "gpt-5-codex",
|
||||
"max_tokens": 1024,
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true, false).unwrap();
|
||||
|
||||
assert_eq!(result["store"], json!(false));
|
||||
assert!(result.get("service_tier").is_none());
|
||||
assert_eq!(result["include"], json!(["reasoning.encrypted_content"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_to_responses_codex_oauth_strips_max_output_tokens() {
|
||||
// ChatGPT Plus/Pro 反代不接受 max_output_tokens(OpenAI 官方 codex-rs 的
|
||||
@@ -1141,7 +1166,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true).unwrap();
|
||||
let result = anthropic_to_responses(input, None, true, true).unwrap();
|
||||
|
||||
assert!(
|
||||
result.get("max_output_tokens").is_none(),
|
||||
@@ -1159,7 +1184,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
|
||||
assert_eq!(result["max_output_tokens"], json!(1024));
|
||||
}
|
||||
@@ -1177,7 +1202,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true).unwrap();
|
||||
let result = anthropic_to_responses(input, None, true, true).unwrap();
|
||||
|
||||
assert!(
|
||||
result.get("temperature").is_none(),
|
||||
@@ -1195,7 +1220,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true).unwrap();
|
||||
let result = anthropic_to_responses(input, None, true, true).unwrap();
|
||||
|
||||
assert!(
|
||||
result.get("top_p").is_none(),
|
||||
@@ -1212,7 +1237,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true).unwrap();
|
||||
let result = anthropic_to_responses(input, None, true, true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result["instructions"],
|
||||
@@ -1246,7 +1271,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true).unwrap();
|
||||
let result = anthropic_to_responses(input, None, true, true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result["instructions"],
|
||||
@@ -1270,7 +1295,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, true).unwrap();
|
||||
let result = anthropic_to_responses(input, None, true, true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result["stream"],
|
||||
@@ -1291,7 +1316,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
|
||||
assert_eq!(result["temperature"], json!(0.7));
|
||||
assert_eq!(result["top_p"], json!(0.9));
|
||||
@@ -1307,7 +1332,7 @@ mod tests {
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
});
|
||||
|
||||
let result = anthropic_to_responses(input, None, false).unwrap();
|
||||
let result = anthropic_to_responses(input, None, false, true).unwrap();
|
||||
|
||||
assert!(
|
||||
result.get("parallel_tool_calls").is_none(),
|
||||
|
||||
@@ -360,10 +360,20 @@ impl StreamCheckService {
|
||||
.as_ref()
|
||||
.and_then(|m| m.provider_type.as_deref())
|
||||
== Some("codex_oauth");
|
||||
let codex_fast_mode = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.map(|m| m.codex_fast_mode_enabled())
|
||||
.unwrap_or(false);
|
||||
|
||||
let body = if is_openai_responses {
|
||||
anthropic_to_responses(anthropic_body, Some(&provider.id), is_codex_oauth)
|
||||
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
||||
anthropic_to_responses(
|
||||
anthropic_body,
|
||||
Some(&provider.id),
|
||||
is_codex_oauth,
|
||||
codex_fast_mode,
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
||||
} else if is_gemini_native {
|
||||
anthropic_to_gemini(anthropic_body)
|
||||
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
||||
|
||||
@@ -82,6 +82,8 @@ interface ClaudeFormFieldsProps {
|
||||
isCodexOauthAuthenticated?: boolean;
|
||||
selectedCodexAccountId?: string | null;
|
||||
onCodexAccountSelect?: (accountId: string | null) => void;
|
||||
codexFastMode?: boolean;
|
||||
onCodexFastModeChange?: (enabled: boolean) => void;
|
||||
|
||||
// Template Values
|
||||
templateValueEntries: Array<[string, TemplateValueConfig]>;
|
||||
@@ -148,6 +150,8 @@ export function ClaudeFormFields({
|
||||
isCodexOauthPreset,
|
||||
selectedCodexAccountId,
|
||||
onCodexAccountSelect,
|
||||
codexFastMode,
|
||||
onCodexFastModeChange,
|
||||
templateValueEntries,
|
||||
templateValues,
|
||||
templatePresetName,
|
||||
@@ -374,6 +378,8 @@ export function ClaudeFormFields({
|
||||
<CodexOAuthSection
|
||||
selectedAccountId={selectedCodexAccountId}
|
||||
onAccountSelect={onCodexAccountSelect}
|
||||
fastModeEnabled={codexFastMode}
|
||||
onFastModeChange={onCodexFastModeChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -30,6 +31,10 @@ interface CodexOAuthSectionProps {
|
||||
selectedAccountId?: string | null;
|
||||
/** 账号选择回调 */
|
||||
onAccountSelect?: (accountId: string | null) => void;
|
||||
/** 是否开启 Codex FAST mode */
|
||||
fastModeEnabled?: boolean;
|
||||
/** FAST mode 切换回调 */
|
||||
onFastModeChange?: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,6 +47,8 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
className,
|
||||
selectedAccountId,
|
||||
onAccountSelect,
|
||||
fastModeEnabled = false,
|
||||
onFastModeChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
@@ -140,6 +147,27 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onFastModeChange && (
|
||||
<div className="flex items-center justify-between rounded-md border bg-muted/30 p-3">
|
||||
<div className="space-y-1 pr-4">
|
||||
<Label className="text-sm font-medium">
|
||||
{t("codexOauth.fastMode", "FAST mode")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("codexOauth.fastModeDescription", {
|
||||
defaultValue:
|
||||
'Send service_tier="priority" for lower latency. Turn it off if the ChatGPT Codex backend rejects the parameter.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fastModeEnabled}
|
||||
onCheckedChange={onFastModeChange}
|
||||
aria-label={t("codexOauth.fastMode", "FAST mode")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 已登录账号列表 */}
|
||||
{hasAnyAccount && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -382,6 +382,9 @@ export function ProviderForm({
|
||||
const [selectedCodexAccountId, setSelectedCodexAccountId] = useState<
|
||||
string | null
|
||||
>(() => resolveManagedAccountId(initialData?.meta, "codex_oauth"));
|
||||
const [codexFastMode, setCodexFastMode] = useState<boolean>(
|
||||
() => initialData?.meta?.codexFastMode ?? false,
|
||||
);
|
||||
|
||||
const {
|
||||
codexAuth,
|
||||
@@ -1115,7 +1118,7 @@ export function ProviderForm({
|
||||
const providerType =
|
||||
templatePreset?.providerType || initialData?.meta?.providerType;
|
||||
|
||||
payload.meta = {
|
||||
const nextMeta: ProviderMeta = {
|
||||
...(baseMeta ?? {}),
|
||||
commonConfigEnabled:
|
||||
appId === "claude"
|
||||
@@ -1146,6 +1149,7 @@ export function ProviderForm({
|
||||
isCopilotProvider && selectedGitHubAccountId
|
||||
? selectedGitHubAccountId
|
||||
: undefined,
|
||||
codexFastMode: isCodexOauthProvider ? codexFastMode : undefined,
|
||||
testConfig: testConfig.enabled ? testConfig : undefined,
|
||||
costMultiplier: pricingConfig.enabled
|
||||
? pricingConfig.costMultiplier
|
||||
@@ -1170,6 +1174,12 @@ export function ProviderForm({
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (!isCodexOauthProvider && "codexFastMode" in nextMeta) {
|
||||
delete nextMeta.codexFastMode;
|
||||
}
|
||||
|
||||
payload.meta = nextMeta;
|
||||
|
||||
await onSubmit(payload);
|
||||
};
|
||||
|
||||
@@ -1735,6 +1745,8 @@ export function ProviderForm({
|
||||
isCodexOauthAuthenticated={isCodexOauthAuthenticated}
|
||||
selectedCodexAccountId={selectedCodexAccountId}
|
||||
onCodexAccountSelect={setSelectedCodexAccountId}
|
||||
codexFastMode={codexFastMode}
|
||||
onCodexFastModeChange={setCodexFastMode}
|
||||
templateValueEntries={templateValueEntries}
|
||||
templateValues={templateValues}
|
||||
templatePresetName={templatePreset?.name || ""}
|
||||
|
||||
@@ -929,7 +929,9 @@
|
||||
"addAnotherAccount": "Add another account",
|
||||
"logoutAll": "Logout all accounts",
|
||||
"retry": "Retry",
|
||||
"copyCode": "Copy code"
|
||||
"copyCode": "Copy code",
|
||||
"fastMode": "FAST mode",
|
||||
"fastModeDescription": "Send service_tier=\"priority\" for lower latency. Off by default — enabling it consumes your ChatGPT quota at a higher rate."
|
||||
},
|
||||
"endpointTest": {
|
||||
"title": "API Endpoint Management",
|
||||
|
||||
@@ -929,7 +929,9 @@
|
||||
"addAnotherAccount": "別のアカウントを追加",
|
||||
"logoutAll": "すべてのアカウントをログアウト",
|
||||
"retry": "再試行",
|
||||
"copyCode": "コードをコピー"
|
||||
"copyCode": "コードをコピー",
|
||||
"fastMode": "FAST モード",
|
||||
"fastModeDescription": "低遅延のため service_tier=\"priority\" を送信します。既定ではオフ——オンにすると ChatGPT のクォータがより速く消費されます。"
|
||||
},
|
||||
"endpointTest": {
|
||||
"title": "API エンドポイント管理",
|
||||
|
||||
@@ -929,7 +929,9 @@
|
||||
"addAnotherAccount": "添加其他账号",
|
||||
"logoutAll": "注销所有账号",
|
||||
"retry": "重试",
|
||||
"copyCode": "复制代码"
|
||||
"copyCode": "复制代码",
|
||||
"fastMode": "FAST 模式",
|
||||
"fastModeDescription": "发送 service_tier=\"priority\" 换取更低延迟。默认关闭——开启后会按更高速率消耗 ChatGPT 配额。"
|
||||
},
|
||||
"endpointTest": {
|
||||
"title": "请求地址管理",
|
||||
|
||||
@@ -155,6 +155,8 @@ export interface ProviderMeta {
|
||||
isFullUrl?: boolean;
|
||||
// Prompt cache key for OpenAI Responses-compatible endpoints (improves cache hit rate)
|
||||
promptCacheKey?: string;
|
||||
// Codex OAuth FAST mode: injects service_tier="priority" on ChatGPT Codex requests
|
||||
codexFastMode?: boolean;
|
||||
// 供应商类型(用于识别 Copilot 等特殊供应商)
|
||||
providerType?: string;
|
||||
// GitHub Copilot 关联账号 ID(旧字段,保留兼容读取)
|
||||
|
||||
Reference in New Issue
Block a user