llmshim 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {llmshim-0.2.1 → llmshim-0.2.2}/CLAUDE.md +1 -1
  2. {llmshim-0.2.1 → llmshim-0.2.2}/Cargo.lock +1 -1
  3. {llmshim-0.2.1 → llmshim-0.2.2}/Cargo.toml +1 -1
  4. {llmshim-0.2.1 → llmshim-0.2.2}/PKG-INFO +1 -1
  5. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/reference/request-fields.md +1 -0
  6. {llmshim-0.2.1 → llmshim-0.2.2}/src/providers/anthropic.rs +17 -2
  7. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_thinking.rs +31 -0
  8. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_anthropic.rs +91 -0
  9. {llmshim-0.2.1 → llmshim-0.2.2}/.github/workflows/pages.yml +0 -0
  10. {llmshim-0.2.1 → llmshim-0.2.2}/.github/workflows/release.yml +0 -0
  11. {llmshim-0.2.1 → llmshim-0.2.2}/.gitignore +0 -0
  12. {llmshim-0.2.1 → llmshim-0.2.2}/LICENSE +0 -0
  13. {llmshim-0.2.1 → llmshim-0.2.2}/README.md +0 -0
  14. {llmshim-0.2.1 → llmshim-0.2.2}/benchmarks/bench.rs +0 -0
  15. {llmshim-0.2.1 → llmshim-0.2.2}/benchmarks/bench_python.py +0 -0
  16. {llmshim-0.2.1 → llmshim-0.2.2}/benchmarks/loadtest.rs +0 -0
  17. {llmshim-0.2.1 → llmshim-0.2.2}/docs/.gitignore +0 -0
  18. {llmshim-0.2.1 → llmshim-0.2.2}/docs/book.toml +0 -0
  19. {llmshim-0.2.1 → llmshim-0.2.2}/docs/mermaid-init.js +0 -0
  20. {llmshim-0.2.1 → llmshim-0.2.2}/docs/mermaid.min.js +0 -0
  21. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/SUMMARY.md +0 -0
  22. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/concepts/contracts.md +0 -0
  23. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/concepts/conversations.md +0 -0
  24. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/concepts/portability.md +0 -0
  25. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/concepts/routing.md +0 -0
  26. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/concepts/translation-flow.md +0 -0
  27. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/guides/fallbacks.md +0 -0
  28. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/guides/images.md +0 -0
  29. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/guides/native-controls.md +0 -0
  30. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/guides/reasoning.md +0 -0
  31. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/guides/streaming.md +0 -0
  32. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/guides/tools.md +0 -0
  33. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/introduction.md +0 -0
  34. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/proxy/deployment.md +0 -0
  35. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/proxy/http-api.md +0 -0
  36. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/proxy/scaling.md +0 -0
  37. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/reference/api.md +0 -0
  38. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/reference/cli.md +0 -0
  39. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/reference/configuration.md +0 -0
  40. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/reference/errors.md +0 -0
  41. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/reference/models.md +0 -0
  42. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/reference/providers.md +0 -0
  43. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/reference/surfaces.md +0 -0
  44. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/start/choose.md +0 -0
  45. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/start/cli.md +0 -0
  46. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/start/clients.md +0 -0
  47. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/start/configure.md +0 -0
  48. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/start/proxy.md +0 -0
  49. {llmshim-0.2.1 → llmshim-0.2.2}/docs/src/start/rust.md +0 -0
  50. {llmshim-0.2.1 → llmshim-0.2.2}/examples/chat.rs +0 -0
  51. {llmshim-0.2.1 → llmshim-0.2.2}/examples/stream.rs +0 -0
  52. {llmshim-0.2.1 → llmshim-0.2.2}/llmshim/__init__.py +0 -0
  53. {llmshim-0.2.1 → llmshim-0.2.2}/llmshim/_client.py +0 -0
  54. {llmshim-0.2.1 → llmshim-0.2.2}/llmshim/_server.py +0 -0
  55. {llmshim-0.2.1 → llmshim-0.2.2}/llmshim/types.py +0 -0
  56. {llmshim-0.2.1 → llmshim-0.2.2}/pyproject.toml +0 -0
  57. {llmshim-0.2.1 → llmshim-0.2.2}/src/client.rs +0 -0
  58. {llmshim-0.2.1 → llmshim-0.2.2}/src/config.rs +0 -0
  59. {llmshim-0.2.1 → llmshim-0.2.2}/src/env.rs +0 -0
  60. {llmshim-0.2.1 → llmshim-0.2.2}/src/error.rs +0 -0
  61. {llmshim-0.2.1 → llmshim-0.2.2}/src/fallback.rs +0 -0
  62. {llmshim-0.2.1 → llmshim-0.2.2}/src/lib.rs +0 -0
  63. {llmshim-0.2.1 → llmshim-0.2.2}/src/log.rs +0 -0
  64. {llmshim-0.2.1 → llmshim-0.2.2}/src/main.rs +0 -0
  65. {llmshim-0.2.1 → llmshim-0.2.2}/src/models.rs +0 -0
  66. {llmshim-0.2.1 → llmshim-0.2.2}/src/provider.rs +0 -0
  67. {llmshim-0.2.1 → llmshim-0.2.2}/src/providers/gemini.rs +0 -0
  68. {llmshim-0.2.1 → llmshim-0.2.2}/src/providers/mod.rs +0 -0
  69. {llmshim-0.2.1 → llmshim-0.2.2}/src/providers/openai.rs +0 -0
  70. {llmshim-0.2.1 → llmshim-0.2.2}/src/providers/xai.rs +0 -0
  71. {llmshim-0.2.1 → llmshim-0.2.2}/src/proxy/convert.rs +0 -0
  72. {llmshim-0.2.1 → llmshim-0.2.2}/src/proxy/error.rs +0 -0
  73. {llmshim-0.2.1 → llmshim-0.2.2}/src/proxy/handlers.rs +0 -0
  74. {llmshim-0.2.1 → llmshim-0.2.2}/src/proxy/mod.rs +0 -0
  75. {llmshim-0.2.1 → llmshim-0.2.2}/src/proxy/ratelimit.rs +0 -0
  76. {llmshim-0.2.1 → llmshim-0.2.2}/src/proxy/types.rs +0 -0
  77. {llmshim-0.2.1 → llmshim-0.2.2}/src/router.rs +0 -0
  78. {llmshim-0.2.1 → llmshim-0.2.2}/src/vision.rs +0 -0
  79. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration.rs +0 -0
  80. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_fallback.rs +0 -0
  81. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_gemini.rs +0 -0
  82. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_gemini_tools.rs +0 -0
  83. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_long_context.rs +0 -0
  84. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_multimodel.rs +0 -0
  85. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_proxy.rs +0 -0
  86. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_tool_roundtrip.rs +0 -0
  87. {llmshim-0.2.1 → llmshim-0.2.2}/tests/integration_vision.rs +0 -0
  88. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_fallback.rs +0 -0
  89. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_fast_mode.rs +0 -0
  90. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_gemini.rs +0 -0
  91. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_log.rs +0 -0
  92. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_models.rs +0 -0
  93. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_multimodel.rs +0 -0
  94. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_openai.rs +0 -0
  95. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_proxy.rs +0 -0
  96. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_proxy_convert.rs +0 -0
  97. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_router.rs +0 -0
  98. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_sse.rs +0 -0
  99. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_tools.rs +0 -0
  100. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_vision.rs +0 -0
  101. {llmshim-0.2.1 → llmshim-0.2.2}/tests/unit_xai.rs +0 -0
@@ -81,7 +81,7 @@ Callers pass provider-specific controls under these keys. Each provider copies w
81
81
 
82
82
  ### Unified reasoning controls
83
83
 
84
- Two knobs work across every provider: `reasoning_effort` (`none|low|medium|high|xhigh|max`) and `reasoning_mode` (`standard|pro`). Each provider transform maps them to its native dialect, **clamping to the nearest tier the target model accepts** (all boundaries verified live — e.g. `max` is native only on OpenAI gpt-5.6; Anthropic 4.6 rejects `xhigh` but has `max`; Gemini's enum tops out at `high`; xAI grok-4.20 models reject any reasoning param). `mode: "pro"` is native on OpenAI gpt-5.6/-pro models (`reasoning.mode`), emulated as a one-tier effort bump elsewhere; explicit `none` always wins. Native passthrough (`x-openai.reasoning`, `x-anthropic.thinking`, `x-gemini.thinkingConfig`) bypasses the mapping entirely and always takes precedence. **Full per-provider mapping tables: `docs/src/guides/reasoning.md`** — update it and the pinning tests in `tests/unit_*.rs` together whenever a mapping changes.
84
+ Two knobs work across every provider: `reasoning_effort` (`none|low|medium|high|xhigh|max`) and `reasoning_mode` (`standard|pro`). A third, `reasoning_summary` (`auto|none`), controls reasoning-text visibility → Anthropic `thinking.display` (`auto`→`summarized`, the default when `reasoning_effort` is present so newer models like Sonnet 5 / Opus 4.7-4.8 return reasoning text instead of the API-default `omitted`; `none`→`omitted` for lower latency). Applies to both the adaptive and pre-4.6 enabled thinking builders; a caller-supplied `thinking` block bypasses it. Each provider transform maps them to its native dialect, **clamping to the nearest tier the target model accepts** (all boundaries verified live — e.g. `max` is native only on OpenAI gpt-5.6; Anthropic 4.6 rejects `xhigh` but has `max`; Gemini's enum tops out at `high`; xAI grok-4.20 models reject any reasoning param). `mode: "pro"` is native on OpenAI gpt-5.6/-pro models (`reasoning.mode`), emulated as a one-tier effort bump elsewhere; explicit `none` always wins. Native passthrough (`x-openai.reasoning`, `x-anthropic.thinking`, `x-gemini.thinkingConfig`) bypasses the mapping entirely and always takes precedence. **Full per-provider mapping tables: `docs/src/guides/reasoning.md`** — update it and the pinning tests in `tests/unit_*.rs` together whenever a mapping changes.
85
85
 
86
86
  ### Tool format translation
87
87
 
@@ -958,7 +958,7 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
958
958
 
959
959
  [[package]]
960
960
  name = "llmshim"
961
- version = "0.2.1"
961
+ version = "0.2.2"
962
962
  dependencies = [
963
963
  "async-stream",
964
964
  "async-trait",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "llmshim"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  edition = "2021"
5
5
  description = "Blazing fast LLM API translation layer in pure Rust"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llmshim
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -23,6 +23,7 @@ renamed or reshaped for the selected provider; unsupported controls are omitted.
23
23
  | `stop` | array of strings | Conditional, mapped | Passed as `stop` to Anthropic or mapped to Gemini `stopSequences`; omitted by OpenAI/xAI |
24
24
  | `reasoning_effort` | string | Portable, mapped/clamped | Unified effort from `none` through `max` |
25
25
  | `reasoning_mode` | string | Portable, mapped/clamped | `standard` or `pro` |
26
+ | `reasoning_summary` | string | Anthropic (`auto`\|`none`) | Reasoning visibility → Anthropic `thinking.display`; `auto`→`summarized` (default with `reasoning_effort`), `none`→`omitted` for lower latency |
26
27
  | `tools` | array | Portable, mapped | OpenAI Chat Completions function schema translated to native tools |
27
28
  | `tool_choice` | string or object | Portable, mapped | Translated where the provider supports tool choice |
28
29
  | `x-openai` | object | Native passthrough | Fields copied to an OpenAI Responses request |
@@ -603,6 +603,17 @@ impl Provider for Anthropic {
603
603
  .unwrap_or(false);
604
604
  let effort = normalize_unified_effort(effort, pro);
605
605
 
606
+ // Reasoning-summary visibility. Newer models (Sonnet 5, Opus
607
+ // 4.7/4.8, ...) default `display` to "omitted" — a signed but
608
+ // empty thinking block with no thinking_delta text. Default to
609
+ // "summarized" so reasoning text is returned consistently across
610
+ // model generations; a latency-sensitive caller opts back into
611
+ // "omitted" via reasoning_summary. Verified live 2026-07-17.
612
+ let display = match obj.get("reasoning_summary").and_then(|v| v.as_str()) {
613
+ Some("none") | Some("omitted") => "omitted",
614
+ _ => "summarized",
615
+ };
616
+
606
617
  if effort == "none" {
607
618
  if Self::uses_adaptive_thinking(model) {
608
619
  // Adaptive models think by default even with no config;
@@ -612,7 +623,10 @@ impl Provider for Anthropic {
612
623
  }
613
624
  // Pre-4.6/Haiku: thinking is opt-in; omitting the key IS "none".
614
625
  } else if Self::uses_adaptive_thinking(model) {
615
- body_obj.insert("thinking".to_string(), json!({"type": "adaptive"}));
626
+ body_obj.insert(
627
+ "thinking".to_string(),
628
+ json!({"type": "adaptive", "display": display}),
629
+ );
616
630
  // Opus/Sonnet 4.6 reject "xhigh" (their tiers: low/medium/high/max);
617
631
  // Opus 4.7/4.8 + Sonnet 5 accept the full low..max range (verified).
618
632
  let anthropic_effort =
@@ -644,7 +658,8 @@ impl Provider for Anthropic {
644
658
  "thinking".to_string(),
645
659
  json!({
646
660
  "type": "enabled",
647
- "budget_tokens": budget
661
+ "budget_tokens": budget,
662
+ "display": display
648
663
  }),
649
664
  );
650
665
  }
@@ -403,3 +403,34 @@ async fn anthropic_reasoning_roundtrip_accepted_live() {
403
403
  assert!(!content.is_empty(), "expected a final answer, got: {resp2}");
404
404
  println!("Round-trip accepted. Final answer: {content}");
405
405
  }
406
+
407
+ #[tokio::test]
408
+ #[ignore]
409
+ async fn anthropic_sonnet5_reasoning_summary_default_returns_text_live() {
410
+ // Issue #37: sonnet-5 defaults display to "omitted" (empty thinking text).
411
+ // With the fix, reasoning_effort defaults display to "summarized" so real
412
+ // reasoning text comes back.
413
+ if std::env::var("ANTHROPIC_API_KEY").is_err() {
414
+ return;
415
+ }
416
+ let router = router();
417
+ // A prompt that reliably engages adaptive thinking (trivial prompts may be
418
+ // answered directly even at high effort — "adaptive" means the model decides).
419
+ let prompt = "Prove or disprove: for every positive integer n, the value \
420
+ n^2 + n + 41 is prime. Give rigorous reasoning.";
421
+ let req = json!({
422
+ "model": "anthropic/claude-sonnet-5",
423
+ "messages": [{"role": "user", "content": prompt}],
424
+ "max_tokens": 4000,
425
+ "reasoning_effort": "high",
426
+ });
427
+ let resp = llmshim::completion(&router, &req).await.unwrap();
428
+ let reasoning = resp["choices"][0]["message"]["reasoning_content"]
429
+ .as_str()
430
+ .unwrap_or("");
431
+ assert!(
432
+ !reasoning.is_empty(),
433
+ "expected reasoning text by default (display=summarized), got empty. resp: {resp}"
434
+ );
435
+ println!("sonnet-5 reasoning text chars: {}", reasoning.len());
436
+ }
@@ -1753,3 +1753,94 @@ fn stream_redacted_thinking_block_start_emits() {
1753
1753
  "blob-stream"
1754
1754
  );
1755
1755
  }
1756
+
1757
+ // ============================================================
1758
+ // Reasoning summary visibility (issue #37): thinking.display
1759
+ // ============================================================
1760
+
1761
+ #[test]
1762
+ fn adaptive_thinking_defaults_display_summarized() {
1763
+ let p = provider();
1764
+ let req = json!({
1765
+ "model": "claude-sonnet-5",
1766
+ "messages": [{"role": "user", "content": "hi"}],
1767
+ "reasoning_effort": "high",
1768
+ });
1769
+ let result = p.transform_request("claude-sonnet-5", &req).unwrap();
1770
+ assert_eq!(result.body["thinking"]["type"], "adaptive");
1771
+ assert_eq!(result.body["thinking"]["display"], "summarized");
1772
+ assert_eq!(result.body["output_config"]["effort"], "high");
1773
+ }
1774
+
1775
+ #[test]
1776
+ fn adaptive_thinking_reasoning_summary_none_omits() {
1777
+ let p = provider();
1778
+ let req = json!({
1779
+ "model": "claude-sonnet-5",
1780
+ "messages": [{"role": "user", "content": "hi"}],
1781
+ "reasoning_effort": "high",
1782
+ "reasoning_summary": "none",
1783
+ });
1784
+ let result = p.transform_request("claude-sonnet-5", &req).unwrap();
1785
+ assert_eq!(result.body["thinking"]["display"], "omitted");
1786
+ // The unified control must not leak into the API body.
1787
+ assert!(result.body.get("reasoning_summary").is_none());
1788
+ }
1789
+
1790
+ #[test]
1791
+ fn adaptive_thinking_reasoning_summary_auto_summarizes() {
1792
+ let p = provider();
1793
+ let req = json!({
1794
+ "model": "claude-opus-4-8",
1795
+ "messages": [{"role": "user", "content": "hi"}],
1796
+ "reasoning_effort": "medium",
1797
+ "reasoning_summary": "auto",
1798
+ });
1799
+ let result = p.transform_request("claude-opus-4-8", &req).unwrap();
1800
+ assert_eq!(result.body["thinking"]["display"], "summarized");
1801
+ }
1802
+
1803
+ #[test]
1804
+ fn enabled_thinking_forwards_display() {
1805
+ // Pre-4.6 / Haiku use the enabled-budget path; display is valid there too.
1806
+ let p = provider();
1807
+ let req = json!({
1808
+ "model": "claude-haiku-4-5-20251001",
1809
+ "messages": [{"role": "user", "content": "hi"}],
1810
+ "reasoning_effort": "high",
1811
+ });
1812
+ let result = p
1813
+ .transform_request("claude-haiku-4-5-20251001", &req)
1814
+ .unwrap();
1815
+ assert_eq!(result.body["thinking"]["type"], "enabled");
1816
+ assert_eq!(result.body["thinking"]["display"], "summarized");
1817
+ assert!(result.body["thinking"]["budget_tokens"].as_u64().unwrap() >= 1024);
1818
+ }
1819
+
1820
+ #[test]
1821
+ fn reasoning_effort_none_stays_disabled_without_display() {
1822
+ let p = provider();
1823
+ let req = json!({
1824
+ "model": "claude-sonnet-5",
1825
+ "messages": [{"role": "user", "content": "hi"}],
1826
+ "reasoning_effort": "none",
1827
+ });
1828
+ let result = p.transform_request("claude-sonnet-5", &req).unwrap();
1829
+ assert_eq!(result.body["thinking"]["type"], "disabled");
1830
+ assert!(result.body["thinking"].get("display").is_none());
1831
+ }
1832
+
1833
+ #[test]
1834
+ fn passthrough_thinking_display_untouched() {
1835
+ // A caller-supplied thinking block wins; the effort translation (and its
1836
+ // default display) is bypassed by the has_thinking gate.
1837
+ let p = provider();
1838
+ let req = json!({
1839
+ "model": "claude-sonnet-5",
1840
+ "messages": [{"role": "user", "content": "hi"}],
1841
+ "reasoning_effort": "high",
1842
+ "thinking": {"type": "adaptive", "display": "omitted"},
1843
+ });
1844
+ let result = p.transform_request("claude-sonnet-5", &req).unwrap();
1845
+ assert_eq!(result.body["thinking"]["display"], "omitted");
1846
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes