diff --git a/openless-all/app/crates/openless-core/src/llm_protocol.rs b/openless-all/app/crates/openless-core/src/llm_protocol.rs
index 987f05781..f5aae2130 100644
--- a/openless-all/app/crates/openless-core/src/llm_protocol.rs
+++ b/openless-all/app/crates/openless-core/src/llm_protocol.rs
@@ -228,13 +228,42 @@ impl LlmProtocolConfig {
Ok(())
}
+ /// Ark accepts Fast on every format. Official OpenAI accepts it only on
+ /// Chat Completions and Responses; Messages and every other provider omit it.
pub(crate) fn apply_service_tier(&self, provider_id: &str, body: &mut Value) {
- if provider_id.trim() == "ark" && self.service_tier == LlmServiceTier::Fast {
+ if self.service_tier != LlmServiceTier::Fast {
+ return;
+ }
+ let provider_id = provider_id.trim();
+ let allowed = provider_id == "ark"
+ || (provider_id == "openai"
+ && matches!(
+ self.format,
+ LlmRequestFormat::ChatCompletions | LlmRequestFormat::Responses
+ ));
+ if allowed {
body["service_tier"] = json!("fast");
}
}
}
+/// Last non-empty tier from an SSE object: top-level `service_tier`, else `response.service_tier`.
+pub(crate) fn observed_response_service_tier(value: &Value) -> Option<&str> {
+ non_empty_tier(value.get("service_tier")).or_else(|| {
+ non_empty_tier(
+ value
+ .get("response")
+ .and_then(|response| response.get("service_tier")),
+ )
+ })
+}
+
+fn non_empty_tier(value: Option<&Value>) -> Option<&str> {
+ value
+ .and_then(Value::as_str)
+ .filter(|tier| !tier.is_empty())
+}
+
fn positive_tokens(value: &str) -> Result {
value
.parse::()
@@ -410,6 +439,8 @@ pub(crate) struct TextEventStream {
pub done: bool,
siwc: bool,
request_id: Option,
+ /// Last non-empty `service_tier` seen on a parsed SSE object.
+ pub(crate) response_service_tier: Option,
reported_tokens: crate::usage_ledger::ReportedTokens,
}
@@ -423,6 +454,7 @@ impl TextEventStream {
done: false,
siwc: false,
request_id: None,
+ response_service_tier: None,
reported_tokens: crate::usage_ledger::ReportedTokens::default(),
}
}
@@ -469,6 +501,9 @@ impl TextEventStream {
}
Err(_) => return Err(response_error("invalid LLM SSE JSON")),
};
+ if let Some(tier) = observed_response_service_tier(&value) {
+ self.response_service_tier = Some(tier.to_string());
+ }
if let Some(reported) = crate::usage_ledger::ReportedTokens::from_response(&value) {
self.reported_tokens.merge_report(reported);
}
@@ -824,7 +859,15 @@ mod tests {
#[test]
fn service_tier_is_validated_and_applied_only_to_ark_requests() {
let mut protocol = LlmProtocolConfig::default();
- assert!(protocol.apply(SERVICE_TIER_ACCOUNT, "invalid").is_err());
+ assert!(protocol.apply(SERVICE_TIER_ACCOUNT, "").is_ok());
+ assert_eq!(protocol.service_tier, LlmServiceTier::Off);
+ assert_eq!(
+ protocol
+ .apply(SERVICE_TIER_ACCOUNT, "invalid")
+ .unwrap_err()
+ .message,
+ "llmServiceTierInvalid"
+ );
protocol.apply(SERVICE_TIER_ACCOUNT, "fast").unwrap();
assert_eq!(protocol.service_tier, LlmServiceTier::Fast);
@@ -847,8 +890,78 @@ mod tests {
config.provider_id = "openai".into();
let body = request_body(&config, false, messages.clone());
- assert!(body.get("service_tier").is_none());
+ if format == LlmRequestFormat::Responses {
+ assert_eq!(body["service_tier"], "fast");
+ } else {
+ assert!(body.get("service_tier").is_none());
+ }
+
+ for provider in ["custom", "opencode", "openai-compatible"] {
+ config.provider_id = provider.into();
+ let body = request_body(&config, false, messages.clone());
+ assert!(
+ body.get("service_tier").is_none(),
+ "{provider} {format:?} must not send service_tier"
+ );
+ }
}
+
+ let config = OpenAICompatibleConfig::new(
+ "openai",
+ "test",
+ "https://api.openai.com/v1",
+ "key",
+ "gpt-5.4",
+ )
+ .with_protocol(LlmProtocolConfig {
+ format: LlmRequestFormat::Responses,
+ service_tier: LlmServiceTier::Off,
+ ..Default::default()
+ });
+ assert!(request_body(&config, false, messages)
+ .get("service_tier")
+ .is_none());
+ }
+
+ #[tokio::test]
+ async fn fast_service_tier_loads_and_unknown_tier_is_rejected() {
+ let store = InMemoryCredentialStore::default();
+ let key = CredentialKey::new(
+ CredentialNamespace::Llm,
+ Some("openai-channel".into()),
+ SERVICE_TIER_ACCOUNT,
+ )
+ .unwrap();
+ store
+ .write(key.clone(), SecretValue::new("fast"))
+ .await
+ .unwrap();
+ assert_eq!(
+ LlmProtocolConfig::load(&store, "openai-channel", "openai")
+ .await
+ .unwrap()
+ .service_tier,
+ LlmServiceTier::Fast
+ );
+ store
+ .write(key.clone(), SecretValue::new("turbo"))
+ .await
+ .unwrap();
+ assert_eq!(
+ LlmProtocolConfig::load(&store, "openai-channel", "openai")
+ .await
+ .unwrap_err()
+ .message,
+ "llmServiceTierInvalid"
+ );
+ store.write(key, SecretValue::new("")).await.unwrap();
+ assert_eq!(
+ LlmProtocolConfig::load(&store, "openai-channel", "openai")
+ .await
+ .unwrap()
+ .service_tier,
+ LlmServiceTier::Off
+ );
}
#[test]
diff --git a/openless-all/app/crates/openless-core/src/polish.rs b/openless-all/app/crates/openless-core/src/polish.rs
index d193333bc..41fd27572 100644
--- a/openless-all/app/crates/openless-core/src/polish.rs
+++ b/openless-all/app/crates/openless-core/src/polish.rs
@@ -906,6 +906,18 @@ impl OpenAICompatibleLLMProvider {
body
}
+ /// `fast` only when this request body actually carries the field.
+ fn requested_service_tier_label(body: &Value) -> &'static str {
+ if body
+ .get("service_tier")
+ .is_some_and(|value| !value.is_null())
+ {
+ "fast"
+ } else {
+ "off"
+ }
+ }
+
/// Shared HTTP send + body parsing. chat_completion / chat_completion_with_polish_history
/// both call here after building their body, avoiding a 30-line send/parse duplication.
/// `budget` is the total budget for this call, chosen by the caller: polish scales
@@ -1005,8 +1017,14 @@ impl OpenAICompatibleLLMProvider {
let extracted =
crate::llm_protocol::extract_text_raw(self.config.protocol.format, &body_text)?;
let cleaned = clean_polish_output(&extracted);
+ let response_tier = serde_json::from_str::(&body_text)
+ .ok()
+ .and_then(|value| {
+ crate::llm_protocol::observed_response_service_tier(&value).map(str::to_string)
+ })
+ .unwrap_or_else(|| "absent".to_string());
log::info!(
- "[llm] request_id={} response provider={} model={} protocol={:?} extracted_chars={} extracted_hash={} cleaned_chars={} cleaned_hash={} extracted_near_duplicates={} cleaned_near_duplicates={}",
+ "[llm] request_id={} response provider={} model={} protocol={:?} extracted_chars={} extracted_hash={} cleaned_chars={} cleaned_hash={} extracted_near_duplicates={} cleaned_near_duplicates={} requested_service_tier={} response_service_tier={}",
request_id,
self.config.provider_id,
self.config.model,
@@ -1017,6 +1035,8 @@ impl OpenAICompatibleLLMProvider {
privacy_text_hash(&cleaned),
find_adjacent_near_duplicates(&extracted).len(),
find_adjacent_near_duplicates(&cleaned).len(),
+ Self::requested_service_tier_label(body),
+ response_tier,
);
Ok(cleaned)
}
@@ -1188,8 +1208,13 @@ impl OpenAICompatibleLLMProvider {
}
self.report_usage(request_id, events.reported_tokens());
let cleaned = clean_polish_output(&full_text);
+ let response_tier = events
+ .response_service_tier
+ .as_deref()
+ .filter(|tier| !tier.is_empty())
+ .unwrap_or("absent");
log::info!(
- "[llm] stream done request_id={} cancelled={} delta_count={} extracted_chars={} extracted_hash={} cleaned_chars={} cleaned_hash={} extracted_near_duplicates={} cleaned_near_duplicates={}",
+ "[llm] stream done request_id={} cancelled={} delta_count={} extracted_chars={} extracted_hash={} cleaned_chars={} cleaned_hash={} extracted_near_duplicates={} cleaned_near_duplicates={} requested_service_tier={} response_service_tier={}",
request_id,
cancelled,
delta_count,
@@ -1199,6 +1224,8 @@ impl OpenAICompatibleLLMProvider {
privacy_text_hash(&cleaned),
find_adjacent_near_duplicates(&full_text).len(),
find_adjacent_near_duplicates(&cleaned).len(),
+ Self::requested_service_tier_label(&body),
+ response_tier,
);
if full_text.is_empty() {
return Err(LLMError::InvalidResponse {
@@ -3844,6 +3871,234 @@ mod tests {
assert_eq!(body["service_tier"], "fast");
}
+ #[test]
+ fn openai_chat_body_includes_fast_service_tier_beside_reasoning_fields() {
+ for (tier, fast) in [
+ (crate::llm_protocol::LlmServiceTier::Fast, true),
+ (crate::llm_protocol::LlmServiceTier::Off, false),
+ ] {
+ let provider = OpenAICompatibleLLMProvider::new(
+ OpenAICompatibleConfig::new(
+ "openai",
+ "OpenAI",
+ "https://api.openai.com/v1",
+ "k",
+ "gpt-5.4",
+ )
+ .with_thinking_enabled(true)
+ .with_protocol(LlmProtocolConfig {
+ service_tier: tier,
+ ..Default::default()
+ }),
+ );
+ let body = provider.chat_body(false, vec![json!({ "role": "user", "content": "hi" })]);
+ assert_eq!(body["model"], "gpt-5.4");
+ assert_eq!(body["messages"][0]["content"], "hi");
+ assert_eq!(body["reasoning_effort"], "medium");
+ if fast {
+ assert_eq!(body["service_tier"], "fast");
+ } else {
+ assert!(body.get("service_tier").is_none());
+ }
+ }
+ }
+
+ #[tokio::test]
+ async fn service_tier_diagnostics_follow_the_request_and_response() {
+ crate::testing::logging::start();
+ for (tier, response_has_tier, requested, returned) in [
+ (
+ crate::llm_protocol::LlmServiceTier::Fast,
+ true,
+ "requested_service_tier=fast",
+ "response_service_tier=fast",
+ ),
+ (
+ crate::llm_protocol::LlmServiceTier::Off,
+ false,
+ "requested_service_tier=off",
+ "response_service_tier=absent",
+ ),
+ ] {
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let addr = listener.local_addr().unwrap();
+ let server = thread::spawn(move || {
+ let (mut stream, _) = listener.accept().unwrap();
+ read_http_request(&mut stream);
+ let body = if response_has_tier {
+ r#"{"service_tier":"fast","choices":[{"message":{"content":"ok"}}]}"#
+ } else {
+ r#"{"choices":[{"message":{"content":"ok"}}]}"#
+ };
+ let _ = write!(
+ stream,
+ "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
+ body.len()
+ );
+ });
+ let session_id = crate::types::SessionId::new();
+ let mut provider = OpenAICompatibleLLMProvider::new(
+ OpenAICompatibleConfig::new(
+ "openai",
+ "OpenAI",
+ format!("http://{addr}"),
+ "k",
+ "gpt-5.4",
+ )
+ .with_protocol(LlmProtocolConfig {
+ service_tier: tier,
+ ..Default::default()
+ }),
+ );
+ provider.set_dictation_session(session_id);
+ let output = provider
+ .chat_completion("sys", "user", std::time::Duration::from_secs(30))
+ .await
+ .unwrap();
+ assert_eq!(output, "ok");
+ server.join().unwrap();
+ let line = response_log_line(session_id);
+ assert!(line.contains(requested), "{line}");
+ assert!(line.contains(returned), "{line}");
+ }
+ }
+
+ #[tokio::test]
+ async fn stream_done_logs_service_tier_literal_or_absent() {
+ crate::testing::logging::start();
+ let cases = [
+ (
+ LlmRequestFormat::ChatCompletions,
+ crate::llm_protocol::LlmServiceTier::Fast,
+ "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}],\"service_tier\":\"fast\"}\n\ndata: [DONE]\n\n",
+ "requested_service_tier=fast",
+ "response_service_tier=fast",
+ ),
+ (
+ LlmRequestFormat::Responses,
+ crate::llm_protocol::LlmServiceTier::Fast,
+ "data: {\"type\":\"response.output_text.delta\",\"delta\":\"ok\",\"service_tier\":\"fast\"}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"service_tier\":\"priority\"}}\n\n",
+ "requested_service_tier=fast",
+ "response_service_tier=priority",
+ ),
+ (
+ LlmRequestFormat::ChatCompletions,
+ crate::llm_protocol::LlmServiceTier::Off,
+ "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\ndata: [DONE]\n\n",
+ "requested_service_tier=off",
+ "response_service_tier=absent",
+ ),
+ ];
+ for (format, tier, sse, requested, returned) in cases {
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let addr = listener.local_addr().unwrap();
+ let payload = sse.to_string();
+ let server = thread::spawn(move || {
+ let (mut stream, _) = listener.accept().unwrap();
+ read_http_request(&mut stream);
+ write_chunked_sse_response(&mut stream, &[payload.as_bytes()]);
+ });
+ let session_id = crate::types::SessionId::new();
+ let mut provider = OpenAICompatibleLLMProvider::new(
+ OpenAICompatibleConfig::new(
+ "openai",
+ "OpenAI",
+ format!("http://{addr}"),
+ "k",
+ "gpt-5.4",
+ )
+ .with_protocol(LlmProtocolConfig {
+ format,
+ service_tier: tier,
+ ..Default::default()
+ }),
+ );
+ provider.set_dictation_session(session_id);
+ let output = provider
+ .chat_completion_messages_streaming(
+ test_messages(),
+ StreamingTimeouts::for_input(1),
+ |_| {},
+ || false,
+ )
+ .await
+ .unwrap();
+ assert_eq!(output, "ok");
+ server.join().unwrap();
+ let line = stream_done_log_line(session_id);
+ assert!(line.contains(requested), "{line}");
+ assert!(line.contains(returned), "{line}");
+ }
+ }
+
+ #[tokio::test]
+ async fn unsupported_service_tier_stays_invalid_response() {
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let addr = listener.local_addr().unwrap();
+ let server = thread::spawn(move || {
+ let (mut stream, _) = listener.accept().unwrap();
+ read_http_request(&mut stream);
+ let body = r#"{"error":{"message":"unsupported service_tier","param":"service_tier"}}"#;
+ let _ = write!(
+ stream,
+ "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
+ body.len()
+ );
+ });
+ let provider = OpenAICompatibleLLMProvider::new(
+ OpenAICompatibleConfig::new(
+ "openai",
+ "OpenAI",
+ format!("http://{addr}"),
+ "k",
+ "gpt-5.4",
+ )
+ .with_protocol(LlmProtocolConfig {
+ service_tier: crate::llm_protocol::LlmServiceTier::Fast,
+ ..Default::default()
+ }),
+ );
+ let error = provider
+ .chat_completion("sys", "user", std::time::Duration::from_secs(30))
+ .await
+ .unwrap_err();
+ server.join().unwrap();
+ let message = error.to_string();
+ match error {
+ LLMError::InvalidResponse { status, body } => {
+ assert_eq!(status, 400);
+ assert!(body.contains("unsupported service_tier"));
+ assert!(message.starts_with("invalid response: status 400, body:"));
+ assert!(message.contains("service_tier"));
+ }
+ other => panic!("expected InvalidResponse, got {other:?}"),
+ }
+ }
+
+ fn response_log_line(session_id: crate::types::SessionId) -> String {
+ let logs = crate::testing::logging::matching(&format!("session_id={session_id} "));
+ let request_id = logs[0]
+ .split_whitespace()
+ .find(|field| field.starts_with("request_id="))
+ .unwrap();
+ crate::testing::logging::matching(request_id)
+ .into_iter()
+ .find(|line| line.contains(" response provider="))
+ .expect("response diagnostic")
+ }
+
+ fn stream_done_log_line(session_id: crate::types::SessionId) -> String {
+ let logs = crate::testing::logging::matching(&format!("session_id={session_id} "));
+ let request_id = logs[0]
+ .split_whitespace()
+ .find(|field| field.starts_with("request_id="))
+ .unwrap();
+ crate::testing::logging::matching(request_id)
+ .into_iter()
+ .find(|line| line.contains("[llm] stream done"))
+ .expect("stream diagnostic")
+ }
+
#[test]
fn openai_chat_body_maps_reasoning_effort_for_openai_reasoning_models() {
for model in [
diff --git a/openless-all/app/linux-egui/src/i18n.rs b/openless-all/app/linux-egui/src/i18n.rs
index d196cf6c4..92f55b11b 100644
--- a/openless-all/app/linux-egui/src/i18n.rs
+++ b/openless-all/app/linux-egui/src/i18n.rs
@@ -370,6 +370,26 @@ pub const CATALOG: &[Msg] = &[
"지원되는 Ark 모델에서만 사용할 수 있으며, 지원되지 않는 모델은 400을 반환할 수 있습니다.",
),
},
+ Msg {
+ key: "settings.providers.openaiServiceTierLabel",
+ text: row(
+ "OpenAI Fast 模式",
+ "OpenAI Fast 模式",
+ "OpenAI Fast mode",
+ "OpenAI Fast モード",
+ "OpenAI Fast 모드",
+ ),
+ },
+ Msg {
+ key: "settings.providers.openaiServiceTierHint",
+ text: row(
+ "Fast 模式费用更高,详见 https://developers.openai.com/api/docs/pricing。不支持的模型或地区会返回错误。",
+ "Fast 模式費用更高,詳見 https://developers.openai.com/api/docs/pricing。不支援的模型或地區會回傳錯誤。",
+ "Fast mode costs more. See https://developers.openai.com/api/docs/pricing. Unsupported models or regions return an error.",
+ "Fast モードは料金が高くなります。https://developers.openai.com/api/docs/pricing を参照してください。未対応のモデルまたはリージョンではエラーが返ります。",
+ "Fast 모드는 요금이 더 높습니다. https://developers.openai.com/api/docs/pricing 를 참고하세요. 지원하지 않는 모델이나 지역은 오류를 반환합니다.",
+ ),
+ },
Msg {
key: "settings.providers.messagesThinkingLabel",
text: row(
diff --git a/openless-all/app/linux-egui/src/main.rs b/openless-all/app/linux-egui/src/main.rs
index ee4bf1a70..c5dca58a5 100644
--- a/openless-all/app/linux-egui/src/main.rs
+++ b/openless-all/app/linux-egui/src/main.rs
@@ -8282,7 +8282,7 @@ mod linux_app {
.zip(&editor.protocol.values)
{
if *account == openless_core::llm_protocol::SERVICE_TIER_ACCOUNT
- && editor.descriptor.provider_type.as_str() != "ark"
+ && !matches!(editor.descriptor.provider_type.as_str(), "ark" | "openai")
{
continue;
}
@@ -8477,7 +8477,7 @@ mod linux_app {
.enumerate()
{
if *account == openless_core::llm_protocol::SERVICE_TIER_ACCOUNT
- && descriptor.provider_type.as_str() != "ark"
+ && !matches!(descriptor.provider_type.as_str(), "ark" | "openai")
{
continue;
}
diff --git a/openless-all/app/linux-egui/src/ui/frontend/settings.rs b/openless-all/app/linux-egui/src/ui/frontend/settings.rs
index 78b7ec399..e94a8061a 100644
--- a/openless-all/app/linux-egui/src/ui/frontend/settings.rs
+++ b/openless-all/app/linux-egui/src/ui/frontend/settings.rs
@@ -4945,12 +4945,23 @@ fn provider_protocol_block(
.color(theme::INK_4),
);
}
- if editor.provider_type == "ark" {
+ let service_tier_copy = match editor.provider_type.as_str() {
+ "ark" => Some((
+ "settings.providers.serviceTierLabel",
+ "settings.providers.serviceTierHint",
+ )),
+ "openai" => Some((
+ "settings.providers.openaiServiceTierLabel",
+ "settings.providers.openaiServiceTierHint",
+ )),
+ _ => None,
+ };
+ if let Some((label_key, hint_key)) = service_tier_copy {
let mut changed = false;
toggle_row(
ui,
- tr_l10n(lang, "settings.providers.serviceTierLabel"),
- tr_l10n(lang, "settings.providers.serviceTierHint"),
+ tr_l10n(lang, label_key),
+ tr_l10n(lang, hint_key),
draft.values[4] == "fast",
|| changed = true,
);
diff --git a/openless-all/app/src/i18n/de.ts b/openless-all/app/src/i18n/de.ts
index 4b21a369c..191cfa63d 100644
--- a/openless-all/app/src/i18n/de.ts
+++ b/openless-all/app/src/i18n/de.ts
@@ -1854,6 +1854,9 @@ export const de: typeof zhCN = {
serviceTierLabel: 'Ark-Inferenz mit niedriger Latenz (fast)',
serviceTierHint:
'Nur unterstützte Ark-Modelle können diese Option verwenden; andere Modelle geben möglicherweise 400 zurück.',
+ openaiServiceTierLabel: 'OpenAI Fast-Modus',
+ openaiServiceTierHint:
+ 'Der Fast-Modus kostet mehr. Siehe https://developers.openai.com/api/docs/pricing. Nicht unterstützte Modelle oder Regionen geben einen Fehler zurück.',
responsesThinkingHint:
'Bei einigen Modellen lässt sich das Denken nur reduzieren, nicht abschalten. Anfragen mit Denken senden keinen Temperaturparameter.',
messagesThinkingHint:
diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts
index 34445f983..56fb9714d 100644
--- a/openless-all/app/src/i18n/en.ts
+++ b/openless-all/app/src/i18n/en.ts
@@ -1819,6 +1819,9 @@ export const en: typeof zhCN = {
serviceTierLabel: 'Ark low-latency inference (fast)',
serviceTierHint:
'Only supported Ark models can use this option; unsupported models may return 400.',
+ openaiServiceTierLabel: 'OpenAI Fast mode',
+ openaiServiceTierHint:
+ 'Fast mode costs more. See https://developers.openai.com/api/docs/pricing. Unsupported models or regions return an error.',
responsesThinkingHint:
'Some models can only reduce thinking, not turn it off. Reasoning requests omit temperature.',
messagesThinkingHint:
diff --git a/openless-all/app/src/i18n/es.ts b/openless-all/app/src/i18n/es.ts
index d3ffe03d6..7a9e7fd58 100644
--- a/openless-all/app/src/i18n/es.ts
+++ b/openless-all/app/src/i18n/es.ts
@@ -1840,6 +1840,9 @@ export const es: typeof zhCN = {
serviceTierLabel: 'Inferencia de baja latencia de Ark (fast)',
serviceTierHint:
'Solo los modelos Ark compatibles pueden usar esta opción; otros pueden devolver 400.',
+ openaiServiceTierLabel: 'Modo Fast de OpenAI',
+ openaiServiceTierHint:
+ 'El modo Fast cuesta más. Consulta https://developers.openai.com/api/docs/pricing. Los modelos o regiones no compatibles devuelven un error.',
responsesThinkingHint:
'Algunos modelos solo permiten reducir el razonamiento, no desactivarlo. Las solicitudes de razonamiento omiten la temperatura.',
messagesThinkingHint:
diff --git a/openless-all/app/src/i18n/fr.ts b/openless-all/app/src/i18n/fr.ts
index f0d1004f0..22de81c3a 100644
--- a/openless-all/app/src/i18n/fr.ts
+++ b/openless-all/app/src/i18n/fr.ts
@@ -1864,6 +1864,9 @@ export const fr: typeof zhCN = {
serviceTierLabel: 'Inférence Ark à faible latence (fast)',
serviceTierHint:
'Seuls les modèles Ark compatibles peuvent utiliser cette option ; les autres peuvent renvoyer 400.',
+ openaiServiceTierLabel: 'Mode Fast OpenAI',
+ openaiServiceTierHint:
+ 'Le mode Fast coûte plus cher. Voir https://developers.openai.com/api/docs/pricing. Les modèles ou régions non pris en charge renvoient une erreur.',
responsesThinkingHint:
'Certains modèles permettent seulement de réduire le raisonnement, pas de le désactiver. Les requêtes de raisonnement omettent la température.',
messagesThinkingHint:
diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts
index 337ca4b29..c908d375c 100644
--- a/openless-all/app/src/i18n/ja.ts
+++ b/openless-all/app/src/i18n/ja.ts
@@ -1804,6 +1804,9 @@ export const ja: typeof zhCN = {
serviceTierLabel: 'Ark 低レイテンシ推論(fast)',
serviceTierHint:
'対応する Ark モデルでのみ利用できます。非対応モデルでは 400 が返る場合があります。',
+ openaiServiceTierLabel: 'OpenAI Fast モード',
+ openaiServiceTierHint:
+ 'Fast モードは料金が高くなります。https://developers.openai.com/api/docs/pricing を参照してください。未対応のモデルまたはリージョンではエラーが返ります。',
responsesThinkingHint:
'一部のモデルでは思考を軽減できますが、完全には無効にできません。推論リクエストでは温度を送信しません。',
messagesThinkingHint:
diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts
index cb4201439..7e57ef9db 100644
--- a/openless-all/app/src/i18n/ko.ts
+++ b/openless-all/app/src/i18n/ko.ts
@@ -1794,6 +1794,9 @@ export const ko: typeof zhCN = {
serviceTierLabel: 'Ark 저지연 추론(fast)',
serviceTierHint:
'지원되는 Ark 모델에서만 사용할 수 있으며, 지원되지 않는 모델은 400을 반환할 수 있습니다.',
+ openaiServiceTierLabel: 'OpenAI Fast 모드',
+ openaiServiceTierHint:
+ 'Fast 모드는 요금이 더 높습니다. https://developers.openai.com/api/docs/pricing 를 참고하세요. 지원하지 않는 모델이나 지역은 오류를 반환합니다.',
responsesThinkingHint:
'일부 모델은 사고를 줄일 수만 있으며 완전히 끌 수 없습니다. 추론 요청에는 온도를 보내지 않습니다.',
messagesThinkingHint:
diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts
index cfb6f4f3b..84e1470ff 100644
--- a/openless-all/app/src/i18n/zh-CN.ts
+++ b/openless-all/app/src/i18n/zh-CN.ts
@@ -1727,6 +1727,9 @@ export const zhCN = {
thinkingBudgetLabel: '思考预算 tokens',
serviceTierLabel: '方舟低延迟推理(fast)',
serviceTierHint: '仅适用于支持低延迟的方舟模型;不支持的模型可能返回 400。',
+ openaiServiceTierLabel: 'OpenAI Fast 模式',
+ openaiServiceTierHint:
+ 'Fast 模式费用更高,详见 https://developers.openai.com/api/docs/pricing。不支持的模型或地区会返回错误。',
responsesThinkingHint: '部分模型只能降低思考,不能完全关闭。推理请求不发送温度参数。',
messagesThinkingHint:
'旧模型或兼容网关可能需要固定预算;思考预算必须小于最大输出。开启思考时不发送温度参数。',
diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts
index 9ab9bed2c..8cac59316 100644
--- a/openless-all/app/src/i18n/zh-TW.ts
+++ b/openless-all/app/src/i18n/zh-TW.ts
@@ -1728,6 +1728,9 @@ export const zhTW: typeof zhCN = {
thinkingBudgetLabel: '思考預算 tokens',
serviceTierLabel: '方舟低延遲推理(fast)',
serviceTierHint: '僅適用於支援低延遲的方舟模型;不支援的模型可能回傳 400。',
+ openaiServiceTierLabel: 'OpenAI Fast 模式',
+ openaiServiceTierHint:
+ 'Fast 模式費用更高,詳見 https://developers.openai.com/api/docs/pricing。不支援的模型或地區會回傳錯誤。',
responsesThinkingHint: '部分模型只能降低思考,無法完全關閉。推理請求不傳送溫度引數。',
messagesThinkingHint:
'舊模型或相容閘道可能需要固定預算;思考預算必須小於最大輸出。開啟思考時不傳送溫度引數。',
diff --git a/openless-all/app/src/pages/settings/LlmProtocolFields.test.ts b/openless-all/app/src/pages/settings/LlmProtocolFields.test.ts
index 536bfa43f..7ee127ba9 100644
--- a/openless-all/app/src/pages/settings/LlmProtocolFields.test.ts
+++ b/openless-all/app/src/pages/settings/LlmProtocolFields.test.ts
@@ -170,6 +170,18 @@ assert(
(await readCredential('ark.service_tier', first)) === null,
'Changing preset clears the ARK-only low-latency option',
);
+const openai = await createChannel('llm', 'openai', 'openai-fast');
+await setCredential('ark.service_tier', 'fast', openai);
+assert(
+ (await readCredential('ark.service_tier', openai)) === 'fast',
+ 'OpenAI Fast mode must survive reload on that channel',
+);
+await setChannelProviderType('llm', openai, 'custom');
+assert(
+ (await readCredential('ark.service_tier', openai)) === null,
+ 'Switching an OpenAI channel to custom clears Fast mode',
+);
+await deleteChannel('llm', openai);
assert(
(await readCredential('ark.api_key', first)) === 'fixture-key',
'Changing preset preserves the key',
diff --git a/openless-all/app/src/pages/settings/LlmProtocolFields.tsx b/openless-all/app/src/pages/settings/LlmProtocolFields.tsx
index 7bafc16bc..f86ec79d1 100644
--- a/openless-all/app/src/pages/settings/LlmProtocolFields.tsx
+++ b/openless-all/app/src/pages/settings/LlmProtocolFields.tsx
@@ -78,8 +78,16 @@ export function LlmProtocolFields({
const [loaded, setLoaded] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<'read' | 'save' | null>(null);
- const arkServiceTier = providerType === 'ark';
- const accounts = arkServiceTier ? [...protocolAccounts, serviceTierAccount] : protocolAccounts;
+ const showsServiceTier = providerType === 'ark' || providerType === 'openai';
+ const serviceTierLabel =
+ providerType === 'openai'
+ ? 'settings.providers.openaiServiceTierLabel'
+ : 'settings.providers.serviceTierLabel';
+ const serviceTierHint =
+ providerType === 'openai'
+ ? 'settings.providers.openaiServiceTierHint'
+ : 'settings.providers.serviceTierHint';
+ const accounts = showsServiceTier ? [...protocolAccounts, serviceTierAccount] : protocolAccounts;
const mounted = useRef(true);
const writing = useRef(false);
const pendingWrite = useRef>(Promise.resolve(true));
@@ -202,15 +210,12 @@ export function LlmProtocolFields({
{t('settings.providers.responsesThinkingHint')}
)}
- {arkServiceTier && (
-
+ {showsServiceTier && (
+
change('ark.service_tier', enabled ? 'fast' : '', true)}
/>