diff --git a/.repository-projection.json b/.repository-projection.json index e189dde04..556060ed9 100644 --- a/.repository-projection.json +++ b/.repository-projection.json @@ -3,11 +3,11 @@ "projection": "deixic-code", "projectionSchemaVersion": 1, "sourceRepository": "dx-corp/mono", - "sourceSha": "06b74765cc5e7353134d868de4140e69fa634f38", + "sourceSha": "5f0f02dc74958fce77b77d166a14dc09ec490804", "destinationRepository": "dx-corp/code", - "priorProjectedBase": "85879e61077a7fd73f0dedcfd21e7c06e7f2f280", + "priorProjectedBase": "acfb663d7933c3028edc4e5315ce353159715c68", "definitionDigest": "82936441c776e3e8edb5d215a75007ec9714a233f489d460075d79d5ef5ba32f", "toolDigest": "45502ff0478e541d02f34cc39ac332935f8d3c0030fa221afe5bcc3b5e51f88e", - "contentDigest": "65e63c3da4396f8ae48cd156df59c171580f0159ba3d1d943b9fe667879f9856", + "contentDigest": "e71c46bc28e9552284f4b3d89e26852771d0d14cee7df7337f4876d4712d8f72", "publicationEligible": true } diff --git a/packages/local-host-rs/src/model_catalog.rs b/packages/local-host-rs/src/model_catalog.rs index 9ff5a00d4..fe686b1ab 100644 --- a/packages/local-host-rs/src/model_catalog.rs +++ b/packages/local-host-rs/src/model_catalog.rs @@ -822,6 +822,18 @@ fn distinct_output_tokens(context: Option, output: Option) -> Option) -> Option { + // Perplexity publishes a 128K context for Sonar Reasoning Pro but no + // 4,096-token output ceiling. The models.dev Perplexity row supplies 4,096 + // for OpenRouter's fallback after its derived 90% value is rejected. + // Keep this exact disputed value unknown until the vendor + // publishes a cap: https://docs.perplexity.ai/docs/sonar/models/sonar-reasoning-pro + if id == "perplexity/sonar-reasoning-pro" && output == Some(4_096) { + return None; + } + output +} + fn parse_limit_tokens(model: &serde_json::Value, field: &str) -> Option { model .get("limit") @@ -1058,9 +1070,12 @@ fn map_openrouter_model( .and_then(serde_json::Value::as_u64) .and_then(|output| u32::try_from(output).ok()) .filter(|output| *output > 0); - let output_tokens = limits.map_or_else( - || distinct_output_tokens(Some(context_tokens), advertised_output), - |limits| limits.distinct_output(id, context_tokens, advertised_output), + let output_tokens = omit_unverified_sonar_output( + id, + limits.map_or_else( + || distinct_output_tokens(Some(context_tokens), advertised_output), + |limits| limits.distinct_output(id, context_tokens, advertised_output), + ), ); let name = model .get("name") @@ -1652,6 +1667,35 @@ mod tests { } } + #[test] + fn unverified_sonar_output_limit_stays_unknown_after_refresh() { + assert_eq!( + catalog_model("perplexity/sonar-reasoning-pro") + .capabilities + .output_tokens, + None + ); + let models_dev = serde_json::json!({ + "perplexity": {"models": { + "sonar-reasoning-pro": {"limit": {"context": 128_000, "output": 4_096}} + }} + }); + let openrouter = serde_json::json!({"data": [ + {"id": "perplexity/sonar-reasoning-pro", "context_length": 128_000, + "top_provider": {"max_completion_tokens": 115_200}}, + {"id": "perplexity/another-model", "context_length": 128_000, + "top_provider": {"max_completion_tokens": 4_096}} + ]}); + let limits = DevTokenLimits::from_models_dev(&models_dev); + let refreshed = map_openrouter_catalog_with_limits(&openrouter, Some(&limits)).unwrap(); + assert_eq!(refreshed[0].capabilities.output_tokens, None); + assert_eq!(refreshed[1].capabilities.output_tokens, Some(4_096)); + assert_eq!( + omit_unverified_sonar_output("perplexity/sonar-reasoning-pro", Some(8_192)), + Some(8_192) + ); + } + #[test] fn managed_model_routes_survive_cache_and_preserve_provider_and_limits() { let mut models = Vec::new(); diff --git a/packages/local-host-rs/src/model_catalog_data.json b/packages/local-host-rs/src/model_catalog_data.json index ca763852b..ff6cfdb26 100644 --- a/packages/local-host-rs/src/model_catalog_data.json +++ b/packages/local-host-rs/src/model_catalog_data.json @@ -9189,7 +9189,6 @@ "reasoning": true, "streaming": true, "context_tokens": 128000, - "output_tokens": 4096, "temperature": true }, "cost": { diff --git a/packages/local-host-rs/src/model_facts_generated.rs b/packages/local-host-rs/src/model_facts_generated.rs index 80d3b2477..36bed8626 100644 --- a/packages/local-host-rs/src/model_facts_generated.rs +++ b/packages/local-host-rs/src/model_facts_generated.rs @@ -344,7 +344,7 @@ pub static MODEL_FACTS: &[(&str, ModelFacts)] = &[ ("perplexity/sonar-deep-research", ModelFacts { context_window: 128_000, max_output_tokens: Some(32_768), input_per_million: Some(2.0), output_per_million: Some(8.0), cache_read_per_million: None, cache_write_per_million: None }), ("perplexity/sonar-pro", ModelFacts { context_window: 200_000, max_output_tokens: Some(8_000), input_per_million: Some(3.0), output_per_million: Some(15.0), cache_read_per_million: None, cache_write_per_million: None }), ("perplexity/sonar-pro-search", ModelFacts { context_window: 200_000, max_output_tokens: Some(8_000), input_per_million: Some(3.0), output_per_million: Some(15.0), cache_read_per_million: None, cache_write_per_million: None }), - ("perplexity/sonar-reasoning-pro", ModelFacts { context_window: 128_000, max_output_tokens: Some(4_096), input_per_million: Some(2.0), output_per_million: Some(8.0), cache_read_per_million: None, cache_write_per_million: None }), + ("perplexity/sonar-reasoning-pro", ModelFacts { context_window: 128_000, max_output_tokens: None, input_per_million: Some(2.0), output_per_million: Some(8.0), cache_read_per_million: None, cache_write_per_million: None }), ("poolside/laguna-s-2.1", ModelFacts { context_window: 1_048_576, max_output_tokens: Some(131_072), input_per_million: Some(0.09), output_per_million: Some(0.18), cache_read_per_million: Some(0.009), cache_write_per_million: None }), ("poolside/laguna-s-2.1:free", ModelFacts { context_window: 262_144, max_output_tokens: Some(32_768), input_per_million: Some(0.0), output_per_million: Some(0.0), cache_read_per_million: None, cache_write_per_million: None }), ("poolside/laguna-xs-2.1", ModelFacts { context_window: 262_144, max_output_tokens: Some(32_768), input_per_million: Some(0.06), output_per_million: Some(0.12), cache_read_per_million: Some(0.03), cache_write_per_million: None }), diff --git a/packages/local-host-rs/src/telemetry/staged_rollout_test.rs b/packages/local-host-rs/src/telemetry/staged_rollout_test.rs index 3036be971..3af244731 100644 --- a/packages/local-host-rs/src/telemetry/staged_rollout_test.rs +++ b/packages/local-host-rs/src/telemetry/staged_rollout_test.rs @@ -127,7 +127,11 @@ fn read_http_request(stream: &mut TcpStream) -> String { let mut request = Vec::new(); loop { let mut chunk = [0_u8; 4096]; - let bytes = stream.read(&mut chunk).expect("read test request"); + let bytes = match stream.read(&mut chunk) { + Ok(bytes) => bytes, + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + Err(error) => panic!("read test request: {error}"), + }; if bytes == 0 { break; } diff --git a/packages/local-host-rs/src/tools/gh.rs b/packages/local-host-rs/src/tools/gh.rs index 8c1d273a7..c1e9fc9a8 100644 --- a/packages/local-host-rs/src/tools/gh.rs +++ b/packages/local-host-rs/src/tools/gh.rs @@ -1218,6 +1218,11 @@ pub(crate) async fn gh_repo( mod tests { use super::*; + // Process scheduling on a loaded CI runner can outlast the command's own + // shutdown bound. Wait for the fake command to start before testing it. + #[cfg(unix)] + const GH_TEST_PROCESS_START_TIMEOUT: Duration = Duration::from_secs(10); + #[cfg(unix)] struct TestGhOverride; @@ -1260,7 +1265,7 @@ mod tests { let execution = tokio::spawn(async move { run_command_output(command, Some(&cancel_for_task)).await }); - tokio::time::timeout(std::time::Duration::from_secs(2), async { + tokio::time::timeout(GH_TEST_PROCESS_START_TIMEOUT, async { while !pid_path.exists() { tokio::time::sleep(std::time::Duration::from_millis(10)).await; } @@ -1325,7 +1330,7 @@ mod tests { ) .await }); - tokio::time::timeout(Duration::from_secs(2), async { + tokio::time::timeout(GH_TEST_PROCESS_START_TIMEOUT, async { while !pid_path.exists() { tokio::time::sleep(Duration::from_millis(10)).await; } @@ -1388,7 +1393,7 @@ mod tests { ) .await }); - tokio::time::timeout(Duration::from_secs(2), async { + tokio::time::timeout(GH_TEST_PROCESS_START_TIMEOUT, async { while !pid_path.exists() { tokio::time::sleep(Duration::from_millis(10)).await; } diff --git a/packages/tui-rs/tests/entrypoint.rs b/packages/tui-rs/tests/entrypoint.rs index eb638101b..86d816ca3 100644 --- a/packages/tui-rs/tests/entrypoint.rs +++ b/packages/tui-rs/tests/entrypoint.rs @@ -29,11 +29,21 @@ fn argv(words: &[&str]) -> Vec { fn every_native_utility_command_routes_to_the_utility_handler() { assert_eq!( NATIVE_UTILITY_COMMANDS.len(), - 45, - "this test's evidence baseline is 45 utility commands; update it deliberately \ + 46, + "this test's evidence baseline is 46 utility commands; update it deliberately \ if the canonical table changes size" ); + let thread_args = argv(&["thread", "attach", "thread-id"]); + assert_eq!( + native_utility_tokens(&thread_args[1..]), + Some(vec![ + "thread".to_owned(), + "attach".to_owned(), + "thread-id".to_owned() + ]) + ); + assert!(NATIVE_UTILITY_COMMANDS.contains(&"experiments")); let experiment_args = argv(&["experiments", "off"]); assert_eq!( diff --git a/packages/ui-rs/src/translations.rs b/packages/ui-rs/src/translations.rs index ebd1d7e7f..12b7da9e7 100644 --- a/packages/ui-rs/src/translations.rs +++ b/packages/ui-rs/src/translations.rs @@ -6666,6 +6666,17 @@ pub(crate) const MESSAGES: &[(&str, [&str; 6])] = &[ "完成", ], ), + ( + "Complete Deixic sign-in in your browser.", + [ + "Completa el inicio de sesión de Deixic en el navegador.", + "Terminez la connexion à Deixic dans votre navigateur.", + "Schließen Sie die Deixic-Anmeldung im Browser ab.", + "ブラウザーで Deixic へのサインインを完了してください。", + "브라우저에서 Deixic 로그인을 완료하세요.", + "请在浏览器中完成 Deixic 登录。", + ], + ), ( "Complete setup to start.", [ @@ -15692,6 +15703,17 @@ pub(crate) const MESSAGES: &[(&str, [&str; 6])] = &[ "偏好设置", ], ), + ( + "Preparing browser sign-in…", + [ + "Preparando el inicio de sesión en el navegador…", + "Préparation de la connexion dans le navigateur…", + "Browser-Anmeldung wird vorbereitet…", + "ブラウザーでのサインインを準備しています…", + "브라우저 로그인을 준비하는 중…", + "正在准备浏览器登录…", + ], + ), ( "Preparing workspace search", [ @@ -15769,6 +15791,17 @@ pub(crate) const MESSAGES: &[(&str, [&str; 6])] = &[ "按 a 以批准控股。", ], ), + ( + "Press c to copy the link or o to open the browser again.", + [ + "Pulsa c para copiar el enlace u o para abrir el navegador de nuevo.", + "Appuyez sur c pour copier le lien ou sur o pour rouvrir le navigateur.", + "Drücken Sie c, um den Link zu kopieren, oder o, um den Browser erneut zu öffnen.", + "c でリンクをコピーするか、o でブラウザーを再度開いてください。", + "c를 눌러 링크를 복사하거나 o를 눌러 브라우저를 다시 여세요.", + "按 c 复制链接,或按 o 再次打开浏览器。", + ], + ), ( "Preview and select a color theme", [ @@ -20738,6 +20771,17 @@ pub(crate) const MESSAGES: &[(&str, [&str; 6])] = &[ "选中的账户已更改 。 完成前再检查一遍", ], ), + ( + "The session will continue when sign-in finishes.", + [ + "La sesión continuará cuando finalice el inicio de sesión.", + "La session reprendra une fois la connexion terminée.", + "Die Sitzung wird fortgesetzt, sobald die Anmeldung abgeschlossen ist.", + "サインインが完了するとセッションが続行されます。", + "로그인이 완료되면 세션이 계속됩니다.", + "登录完成后,会话将继续。", + ], + ), ( "The test sends a small fixed request to your selected model and may incur usage charges. It does not send repository contents.", [ @@ -23842,6 +23886,17 @@ pub(crate) const MESSAGES: &[(&str, [&str; 6])] = &[ "等待ChatGPT登录完成...", ], ), + ( + "Waiting for browser sign-in…", + [ + "Esperando el inicio de sesión en el navegador…", + "En attente de la connexion dans le navigateur…", + "Warten auf die Browser-Anmeldung…", + "ブラウザーでのサインインを待っています…", + "브라우저 로그인을 기다리는 중…", + "正在等待浏览器登录…", + ], + ), ( "Waiting for the browser callback…", [ @@ -24744,6 +24799,17 @@ pub(crate) const MESSAGES: &[(&str, [&str; 6])] = &[ "内置专家简介必须来自汇编的指令", ], ), + ( + "c copy o open u show link enter retry esc close", + [ + "c copiar o abrir u mostrar enlace enter reintentar esc cerrar", + "c copier o ouvrir u afficher le lien enter réessayer esc fermer", + "c kopieren o öffnen u Link anzeigen enter erneut versuchen esc schließen", + "c コピー o 開く u リンク表示 enter 再試行 esc 閉じる", + "c 복사 o 열기 u 링크 표시 enter 재시도 esc 닫기", + "c 复制 o 打开 u 显示链接 enter 重试 esc 关闭", + ], + ), ( "cancel", [ diff --git a/scripts/catalog-output-limits.mjs b/scripts/catalog-output-limits.mjs new file mode 100644 index 000000000..99f3c194c --- /dev/null +++ b/scripts/catalog-output-limits.mjs @@ -0,0 +1,12 @@ +/** Keep disputed aggregator output caps unknown until a vendor publishes them. */ +export function omitUnverifiedSonarOutput(id, output) { + // Perplexity documents a 128K context for Sonar Reasoning Pro, but no + // 4,096-token output ceiling. OpenRouter's derived 90%-of-context value + // falls back to the models.dev Perplexity row, which supplies 4,096. + // Scope the omission to this exact model and disputed value. + // https://docs.perplexity.ai/docs/sonar/models/sonar-reasoning-pro + if (id === "perplexity/sonar-reasoning-pro" && output === 4096) { + return undefined; + } + return output; +} diff --git a/scripts/catalog-output-limits.test.mjs b/scripts/catalog-output-limits.test.mjs new file mode 100644 index 000000000..7c9cf96d8 --- /dev/null +++ b/scripts/catalog-output-limits.test.mjs @@ -0,0 +1,10 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { omitUnverifiedSonarOutput } from "./catalog-output-limits.mjs"; + +test("only the unverified Sonar Reasoning Pro output cap is omitted", () => { + assert.equal(omitUnverifiedSonarOutput("perplexity/sonar-reasoning-pro", 4096), undefined); + assert.equal(omitUnverifiedSonarOutput("perplexity/sonar-reasoning-pro", 8192), 8192); + assert.equal(omitUnverifiedSonarOutput("perplexity/another-model", 4096), 4096); +}); diff --git a/scripts/fetch-model-catalog.mjs b/scripts/fetch-model-catalog.mjs index 1468ba3c0..4f6eedb99 100644 --- a/scripts/fetch-model-catalog.mjs +++ b/scripts/fetch-model-catalog.mjs @@ -1,5 +1,7 @@ #!/usr/bin/env node +import { omitUnverifiedSonarOutput } from "./catalog-output-limits.mjs"; + /** * Regenerate the bundled model catalog snapshot consumed by * `packages/local-host-rs/src/model_catalog.rs` via `include_str!`. @@ -244,7 +246,7 @@ function mapOpenRouterModel(model, tokenLimits, vendorFacts) { (model.reasoning != null && typeof model.reasoning === "object"), streaming: true, context_tokens: context, - output_tokens: resolveOpenRouterOutput(tokenLimits, id, context, advertised), + output_tokens: omitUnverifiedSonarOutput(id, resolveOpenRouterOutput(tokenLimits, id, context, advertised)), // OpenRouter lists `temperature` in `supported_parameters` for // routes that accept it. Omitted when the route lists no // parameters at all.