sqlseed-ai 0.2.4__tar.gz → 0.2.6__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 (149) hide show
  1. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/.gitignore +10 -0
  2. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/PKG-INFO +77 -11
  3. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/README.md +74 -8
  4. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/README.zh-CN.md +57 -10
  5. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/pyproject.toml +2 -2
  6. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_client.py +38 -2
  7. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_hardware.py +65 -14
  8. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_json_utils.py +69 -29
  9. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_prompts.py +19 -3
  10. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_caller.py +69 -15
  11. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_context.py +17 -9
  12. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_json_parser.py +3 -2
  13. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_streaming.py +103 -21
  14. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/_tool_calling.py +49 -26
  15. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/cli/ai_commands.py +59 -27
  16. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/config.py +6 -1
  17. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/errors.py +10 -1
  18. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/level2_column_healer.py +7 -6
  19. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/mcp.py +64 -31
  20. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/refiner.py +73 -14
  21. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/runtime.py +2 -2
  22. sqlseed_ai-0.2.6/tests/healer/conftest.py +66 -0
  23. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/test_heal_orchestrator_real.py +2 -0
  24. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/test_level1_subgraph_healer_real.py +2 -0
  25. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/test_level2_column_healer_real.py +1 -0
  26. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/test_level3_compact_healer_real.py +3 -0
  27. sqlseed_ai-0.2.6/tests/healer/test_llm_availability.py +112 -0
  28. sqlseed_ai-0.2.6/tests/http_helpers.py +23 -0
  29. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_caller.py +20 -0
  30. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_client.py +3 -3
  31. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_hardware.py +154 -4
  32. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_json_utils.py +59 -0
  33. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_plugin.py +9 -6
  34. sqlseed_ai-0.2.6/tests/test_ai_preserve_names.py +465 -0
  35. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_prompts_tools.py +16 -0
  36. sqlseed_ai-0.2.6/tests/test_analyzer_client_lifecycle.py +122 -0
  37. sqlseed_ai-0.2.6/tests/test_cli_suggestion_diagnostics.py +211 -0
  38. sqlseed_ai-0.2.6/tests/test_cli_suggestion_identity.py +139 -0
  39. sqlseed_ai-0.2.6/tests/test_client_loopback.py +170 -0
  40. sqlseed_ai-0.2.6/tests/test_http_probe_lifecycle.py +66 -0
  41. sqlseed_ai-0.2.6/tests/test_mcp.py +228 -0
  42. sqlseed_ai-0.2.6/tests/test_mcp_error_redaction.py +29 -0
  43. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_mcp_stdio.py +9 -11
  44. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_nonweb_ai_boundaries.py +23 -3
  45. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_quality_error_boundaries.py +24 -0
  46. sqlseed_ai-0.2.6/tests/test_real_llm_environment.py +58 -0
  47. sqlseed_ai-0.2.6/tests/test_refiner_json_recovery.py +200 -0
  48. sqlseed_ai-0.2.6/tests/test_refiner_table_boundaries.py +317 -0
  49. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_runtime.py +12 -9
  50. sqlseed_ai-0.2.4/tests/healer/conftest.py +0 -57
  51. sqlseed_ai-0.2.4/tests/healer/test_llm_availability.py +0 -40
  52. sqlseed_ai-0.2.4/tests/test_mcp.py +0 -121
  53. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/LICENSE +0 -0
  54. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/__init__.py +0 -0
  55. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_generator_names.py +0 -0
  56. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_model_selector.py +0 -0
  57. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/_tools.py +0 -0
  58. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/ai_mediator.py +0 -0
  59. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/analyzer/__init__.py +0 -0
  60. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/__init__.py +0 -0
  61. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/_check_inference.py +0 -0
  62. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/_cross_column_checks.py +0 -0
  63. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/orchestrator.py +0 -0
  64. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/auto_heal/time_budget.py +0 -0
  65. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/cli/__init__.py +0 -0
  66. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/contracts/__init__.py +0 -0
  67. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/contracts/builtin_violations.py +0 -0
  68. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/contracts/matrix.py +0 -0
  69. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/contracts/registry.py +0 -0
  70. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/examples.py +0 -0
  71. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/exceptions.py +0 -0
  72. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/__init__.py +0 -0
  73. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/_client.py +0 -0
  74. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/_llm_call.py +0 -0
  75. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/candidate_validation.py +0 -0
  76. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/context_detector.py +0 -0
  77. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/degrader.py +0 -0
  78. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/diff_learner.py +0 -0
  79. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/failure_classifier.py +0 -0
  80. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/level1_subgraph_healer.py +0 -0
  81. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/level3_compact_healer.py +0 -0
  82. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/models.py +0 -0
  83. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/orchestrator.py +0 -0
  84. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/oscillation.py +0 -0
  85. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/post_repair.py +0 -0
  86. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/healer/subgraph.py +0 -0
  87. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/__init__.py +0 -0
  88. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/executor.py +0 -0
  89. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/models.py +0 -0
  90. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/pipeline.py +0 -0
  91. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/repair/strategies.py +0 -0
  92. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/__init__.py +0 -0
  93. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/composite_fk.py +0 -0
  94. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/cross_column.py +0 -0
  95. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/dialect_parser.py +0 -0
  96. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/main.py +0 -0
  97. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/models.py +0 -0
  98. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/schema_snapshot.py +0 -0
  99. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/shadow_fk_scan.py +0 -0
  100. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/src/sqlseed_ai/validator/single_column.py +0 -0
  101. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/.pylintrc +0 -0
  102. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/__init__.py +0 -0
  103. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/conftest.py +0 -0
  104. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/__init__.py +0 -0
  105. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/scenario_helpers.py +0 -0
  106. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/test_context_detector.py +0 -0
  107. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/test_failure_classifier.py +0 -0
  108. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/test_level2_context_builder.py +0 -0
  109. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/healer/test_llm_call_boundary.py +0 -0
  110. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/property/__init__.py +0 -0
  111. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/property/test_matrix_completeness.py +0 -0
  112. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/schema_helpers.py +0 -0
  113. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_analyzer_streaming.py +0 -0
  114. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_commands.py +0 -0
  115. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_config.py +0 -0
  116. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_errors.py +0 -0
  117. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_mediator.py +0 -0
  118. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_model_selector.py +0 -0
  119. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_plugin_init.py +0 -0
  120. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_ai_tool_calling.py +0 -0
  121. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_auto_heal_orchestrator.py +0 -0
  122. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_auto_heal_sonar_boundaries.py +0 -0
  123. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_auto_heal_time_budget.py +0 -0
  124. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_cli_auto_heal.py +0 -0
  125. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_cli_input_contract.py +0 -0
  126. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_contracts_builtin.py +0 -0
  127. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_contracts_matrix.py +0 -0
  128. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_contracts_registry.py +0 -0
  129. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_healer_candidate_contract.py +0 -0
  130. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_healer_degrader.py +0 -0
  131. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_healer_diff_learner.py +0 -0
  132. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_healer_models.py +0 -0
  133. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_healer_oscillation.py +0 -0
  134. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_healer_post_repair.py +0 -0
  135. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_healer_subgraph.py +0 -0
  136. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_prompts_p0_p3.py +0 -0
  137. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_refiner.py +0 -0
  138. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_repair_executor.py +0 -0
  139. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_repair_models.py +0 -0
  140. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_repair_pipeline.py +0 -0
  141. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_repair_strategies.py +0 -0
  142. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_schema_snapshot.py +0 -0
  143. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_validator_composite_fk.py +0 -0
  144. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_validator_cross_column.py +0 -0
  145. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_validator_dialect_parser.py +0 -0
  146. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_validator_main.py +0 -0
  147. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_validator_models.py +0 -0
  148. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_validator_shadow_fk_scan.py +0 -0
  149. {sqlseed_ai-0.2.4 → sqlseed_ai-0.2.6}/tests/test_validator_single_column.py +0 -0
@@ -59,6 +59,16 @@ dmypy.json
59
59
  *.db
60
60
  *.sqlite
61
61
  *.sqlite3
62
+ # SQLite runtime sidecars
63
+ *.db-wal
64
+ *.db-shm
65
+ *.db-journal
66
+ *.sqlite-wal
67
+ *.sqlite-shm
68
+ *.sqlite-journal
69
+ *.sqlite3-wal
70
+ *.sqlite3-shm
71
+ *.sqlite3-journal
62
72
  .sqlseed_cache/
63
73
  snapshots/
64
74
  .env
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlseed-ai
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Optional LLM schema analysis and contract-driven configuration repair for sqlseed
5
5
  Project-URL: Documentation, https://sunbos.github.io/sqlseed/gemma4-integration/
6
6
  Project-URL: Homepage, https://github.com/sunbos/sqlseed
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Python: >=3.10
19
19
  Requires-Dist: httpx>=0.24.0
20
20
  Requires-Dist: networkx>=3.0
21
- Requires-Dist: openai>=1.0
21
+ Requires-Dist: openai>=1.55.3
22
22
  Requires-Dist: sqlseed-cli<0.3,>=0.2.4.dev0
23
- Requires-Dist: sqlseed<0.3,>=0.2.4.dev0
23
+ Requires-Dist: sqlseed<0.3,>=0.2.5.dev0
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: hypothesis>=6.100; extra == 'dev'
26
26
  Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
@@ -46,13 +46,15 @@ backend test; installing the plugin does not perform one.
46
46
 
47
47
  ## Installation
48
48
 
49
- For the 0.2.4 release, use a Python 3.10+ virtual environment:
49
+ These instructions target version 0.2.6. Check [Releases](https://github.com/sunbos/sqlseed/releases)
50
+ for publication status; use the source installation below to test an unpublished candidate.
51
+ Use a Python 3.10+ virtual environment:
50
52
 
51
53
  ```bash
52
- python -m pip install "sqlseed-ai==0.2.4"
54
+ python -m pip install "sqlseed-ai==0.2.6"
53
55
  ```
54
56
 
55
- Core 0.2.3 lacks the plugin hooks and target-validation interfaces used here.
57
+ Core 0.2.4 and older lack the shared diagnostic interfaces required by this version.
56
58
  For development, install Core and the required local plugins together from the
57
59
  repository root:
58
60
 
@@ -100,6 +102,28 @@ names fail before output is written. `--merge` requires `--output`; it replaces
100
102
  selected tables, retains existing dependency and unrelated tables plus root settings,
101
103
  and appends missing generated tables.
102
104
 
105
+ Single-table `ai-suggest` checks that the target exists before contacting the
106
+ model. Suggestions and cached results must name that same table, including when
107
+ using `--no-verify` or `--max-retries 0`. SQLite case aliases remain supported;
108
+ export preserves real table and column names, including leading punctuation.
109
+ Rejected suggestions leave existing output files unchanged.
110
+
111
+ The prompt requests one JSON object configuring only the named table, preserving
112
+ its table and column names. Other table names are reference context, not additional
113
+ output targets; the response still passes target validation.
114
+
115
+ Direct analysis (`--no-verify` or `--max-retries 0`), streaming or non-streaming,
116
+ reports empty replies, invalid JSON, output-limit truncation, and empty configuration
117
+ objects separately, without
118
+ echoing the model response in these diagnostics. It announces a retry only when
119
+ another existing shorter-prompt level remains. Once those levels are exhausted,
120
+ it reports the final cause and exits unsuccessfully; it does not increase the
121
+ request budget or change the existing output YAML or database.
122
+
123
+ Direct Python callers can pass `preserve_names=True` to
124
+ `SchemaAnalyzer.call_llm()` or `call_llm_streaming()` before validating against
125
+ their schema. The default retains the existing leading-punctuation cleanup.
126
+
103
127
  `auto-heal --config` reads that document. An explicit `--db` or `--url` selects the
104
128
  output connection. Invalid YAML, configuration structure, or unknown input tables
105
129
  fail without replacing the output file. Candidate repairs are checked for config
@@ -112,7 +136,7 @@ actual generated values and database constraints.
112
136
  The AI MCP entry point requires the `mcp` extra:
113
137
 
114
138
  ```bash
115
- python -m pip install "sqlseed-ai[mcp]==0.2.4"
139
+ python -m pip install "sqlseed-ai[mcp]==0.2.6"
116
140
  mcp-server-sqlseed-ai
117
141
  ```
118
142
 
@@ -145,15 +169,57 @@ explicit backend, then known URL patterns, then OpenAI-compatible behavior. It d
145
169
  not probe every service as a fallback chain. The `tool_calling_protocol` setting and
146
170
  its resolver choose the response protocol; a model name alone is insufficient.
147
171
 
148
- AI configuration caches include schema hashes. Schema changes invalidate cached
149
- suggestions; `--no-cache` bypasses them. Review model output before writing data.
172
+ AI requests to `localhost` and loopback IP addresses connect directly even when
173
+ an HTTP proxy is configured. Remote services keep the environment proxy settings;
174
+ `SSL_CERT_FILE` and `SSL_CERT_DIR` remain effective for HTTPS certificate validation.
175
+
176
+ For Python callers, `SchemaAnalyzer.call_llm(..., strict_json=True)` and
177
+ `call_llm_streaming(..., strict_json=True)` distinguish empty replies, invalid JSON,
178
+ and output-limit truncation using content-free `JSONResponseError.code` values.
179
+ JSON parsing can complete missing final `}` or `]`
180
+ delimiters, including inside code fences, but never fills missing values or strings.
181
+ An output-limit response is rejected even if its prefix parses, including a streaming
182
+ length marker in a separate empty terminal chunk. Both methods default to
183
+ `strict_json=False`. Strict local calls request JSON sampling constraints: LM Studio
184
+ uses its JSON-schema grammar interface, and Ollama uses JSON object mode. A server
185
+ that explicitly rejects the format gets one text-mode compatibility attempt;
186
+ the same strict parser still rejects invalid or truncated output. The CLI's direct
187
+ path and both refiner modes keep their existing prompt/refinement retry budgets.
188
+ Strict tool calling also rejects arrays, scalars and `null` arguments as `invalid_json`;
189
+ compatibility mode may still fall back to response text. Parsed suggestions still
190
+ require scope and rule validation.
191
+
192
+ Single-table suggestion caches use a versioned hash of sorted column names, encoded
193
+ as a JSON array so names containing delimiters remain distinct. The check detects
194
+ added, removed, or renamed columns; column order, types, and constraints are not
195
+ included. Caches using the older delimiter encoding are ignored and regenerated.
196
+ Use `--no-cache` to analyze again without cached suggestions. This cache check is
197
+ separate from AutoHeal's full schema fingerprint. Malformed cache metadata
198
+ or configuration containers are treated as cache misses. Review model output before
199
+ writing data.
200
+
201
+ ## Hardware estimates on macOS
202
+
203
+ The AI MCP model list distinguishes Apple unified memory, Intel shared graphics
204
+ memory, and dedicated GPU memory. Apple unified RAM is not reported as dedicated
205
+ VRAM or added to system RAM. `unified_memory_budget_gb` is a static heuristic:
206
+ `max(0, min(total_ram_gb * 0.75, total_ram_gb - 4))`, reserving at least 4 GiB or
207
+ 25% for the system. It is not measured free memory or a Metal allocation limit.
208
+
209
+ For an identified Apple GPU on macOS, model screening compares that budget with
210
+ both existing minimum RAM and VRAM estimates and reports at most `capable`.
211
+ This does not verify Metal acceleration, backend/model support, or successful
212
+ inference; loaded applications and context size can require more memory.
150
213
 
151
214
  ## Requirements
152
215
 
216
+ These metadata requirements apply to version 0.2.6 and its source candidates.
217
+ Use local Core and plugins together when developing from source.
218
+
153
219
  - Python `>=3.10`
154
- - `sqlseed>=0.2.4.dev0,<0.3`
220
+ - `sqlseed>=0.2.5.dev0,<0.3`
155
221
  - `sqlseed-cli>=0.2.4.dev0,<0.3`
156
- - `openai>=1.0`
222
+ - `openai>=1.55.3` (SDK transport defaults with HTTPX 0.28 compatibility)
157
223
  - `httpx>=0.24.0`
158
224
  - `networkx>=3.0`
159
225
  - Optional `mcp` extra: `mcp>=1.0,<2`
@@ -14,13 +14,15 @@ backend test; installing the plugin does not perform one.
14
14
 
15
15
  ## Installation
16
16
 
17
- For the 0.2.4 release, use a Python 3.10+ virtual environment:
17
+ These instructions target version 0.2.6. Check [Releases](https://github.com/sunbos/sqlseed/releases)
18
+ for publication status; use the source installation below to test an unpublished candidate.
19
+ Use a Python 3.10+ virtual environment:
18
20
 
19
21
  ```bash
20
- python -m pip install "sqlseed-ai==0.2.4"
22
+ python -m pip install "sqlseed-ai==0.2.6"
21
23
  ```
22
24
 
23
- Core 0.2.3 lacks the plugin hooks and target-validation interfaces used here.
25
+ Core 0.2.4 and older lack the shared diagnostic interfaces required by this version.
24
26
  For development, install Core and the required local plugins together from the
25
27
  repository root:
26
28
 
@@ -68,6 +70,28 @@ names fail before output is written. `--merge` requires `--output`; it replaces
68
70
  selected tables, retains existing dependency and unrelated tables plus root settings,
69
71
  and appends missing generated tables.
70
72
 
73
+ Single-table `ai-suggest` checks that the target exists before contacting the
74
+ model. Suggestions and cached results must name that same table, including when
75
+ using `--no-verify` or `--max-retries 0`. SQLite case aliases remain supported;
76
+ export preserves real table and column names, including leading punctuation.
77
+ Rejected suggestions leave existing output files unchanged.
78
+
79
+ The prompt requests one JSON object configuring only the named table, preserving
80
+ its table and column names. Other table names are reference context, not additional
81
+ output targets; the response still passes target validation.
82
+
83
+ Direct analysis (`--no-verify` or `--max-retries 0`), streaming or non-streaming,
84
+ reports empty replies, invalid JSON, output-limit truncation, and empty configuration
85
+ objects separately, without
86
+ echoing the model response in these diagnostics. It announces a retry only when
87
+ another existing shorter-prompt level remains. Once those levels are exhausted,
88
+ it reports the final cause and exits unsuccessfully; it does not increase the
89
+ request budget or change the existing output YAML or database.
90
+
91
+ Direct Python callers can pass `preserve_names=True` to
92
+ `SchemaAnalyzer.call_llm()` or `call_llm_streaming()` before validating against
93
+ their schema. The default retains the existing leading-punctuation cleanup.
94
+
71
95
  `auto-heal --config` reads that document. An explicit `--db` or `--url` selects the
72
96
  output connection. Invalid YAML, configuration structure, or unknown input tables
73
97
  fail without replacing the output file. Candidate repairs are checked for config
@@ -80,7 +104,7 @@ actual generated values and database constraints.
80
104
  The AI MCP entry point requires the `mcp` extra:
81
105
 
82
106
  ```bash
83
- python -m pip install "sqlseed-ai[mcp]==0.2.4"
107
+ python -m pip install "sqlseed-ai[mcp]==0.2.6"
84
108
  mcp-server-sqlseed-ai
85
109
  ```
86
110
 
@@ -113,15 +137,57 @@ explicit backend, then known URL patterns, then OpenAI-compatible behavior. It d
113
137
  not probe every service as a fallback chain. The `tool_calling_protocol` setting and
114
138
  its resolver choose the response protocol; a model name alone is insufficient.
115
139
 
116
- AI configuration caches include schema hashes. Schema changes invalidate cached
117
- suggestions; `--no-cache` bypasses them. Review model output before writing data.
140
+ AI requests to `localhost` and loopback IP addresses connect directly even when
141
+ an HTTP proxy is configured. Remote services keep the environment proxy settings;
142
+ `SSL_CERT_FILE` and `SSL_CERT_DIR` remain effective for HTTPS certificate validation.
143
+
144
+ For Python callers, `SchemaAnalyzer.call_llm(..., strict_json=True)` and
145
+ `call_llm_streaming(..., strict_json=True)` distinguish empty replies, invalid JSON,
146
+ and output-limit truncation using content-free `JSONResponseError.code` values.
147
+ JSON parsing can complete missing final `}` or `]`
148
+ delimiters, including inside code fences, but never fills missing values or strings.
149
+ An output-limit response is rejected even if its prefix parses, including a streaming
150
+ length marker in a separate empty terminal chunk. Both methods default to
151
+ `strict_json=False`. Strict local calls request JSON sampling constraints: LM Studio
152
+ uses its JSON-schema grammar interface, and Ollama uses JSON object mode. A server
153
+ that explicitly rejects the format gets one text-mode compatibility attempt;
154
+ the same strict parser still rejects invalid or truncated output. The CLI's direct
155
+ path and both refiner modes keep their existing prompt/refinement retry budgets.
156
+ Strict tool calling also rejects arrays, scalars and `null` arguments as `invalid_json`;
157
+ compatibility mode may still fall back to response text. Parsed suggestions still
158
+ require scope and rule validation.
159
+
160
+ Single-table suggestion caches use a versioned hash of sorted column names, encoded
161
+ as a JSON array so names containing delimiters remain distinct. The check detects
162
+ added, removed, or renamed columns; column order, types, and constraints are not
163
+ included. Caches using the older delimiter encoding are ignored and regenerated.
164
+ Use `--no-cache` to analyze again without cached suggestions. This cache check is
165
+ separate from AutoHeal's full schema fingerprint. Malformed cache metadata
166
+ or configuration containers are treated as cache misses. Review model output before
167
+ writing data.
168
+
169
+ ## Hardware estimates on macOS
170
+
171
+ The AI MCP model list distinguishes Apple unified memory, Intel shared graphics
172
+ memory, and dedicated GPU memory. Apple unified RAM is not reported as dedicated
173
+ VRAM or added to system RAM. `unified_memory_budget_gb` is a static heuristic:
174
+ `max(0, min(total_ram_gb * 0.75, total_ram_gb - 4))`, reserving at least 4 GiB or
175
+ 25% for the system. It is not measured free memory or a Metal allocation limit.
176
+
177
+ For an identified Apple GPU on macOS, model screening compares that budget with
178
+ both existing minimum RAM and VRAM estimates and reports at most `capable`.
179
+ This does not verify Metal acceleration, backend/model support, or successful
180
+ inference; loaded applications and context size can require more memory.
118
181
 
119
182
  ## Requirements
120
183
 
184
+ These metadata requirements apply to version 0.2.6 and its source candidates.
185
+ Use local Core and plugins together when developing from source.
186
+
121
187
  - Python `>=3.10`
122
- - `sqlseed>=0.2.4.dev0,<0.3`
188
+ - `sqlseed>=0.2.5.dev0,<0.3`
123
189
  - `sqlseed-cli>=0.2.4.dev0,<0.3`
124
- - `openai>=1.0`
190
+ - `openai>=1.55.3` (SDK transport defaults with HTTPX 0.28 compatibility)
125
191
  - `httpx>=0.24.0`
126
192
  - `networkx>=3.0`
127
193
  - Optional `mcp` extra: `mcp>=1.0,<2`
@@ -3,7 +3,7 @@
3
3
  [English](https://github.com/sunbos/sqlseed/blob/main/plugins/sqlseed-ai/README.md) |
4
4
  **[中文](https://github.com/sunbos/sqlseed/blob/main/plugins/sqlseed-ai/README.zh-CN.md)**
5
5
 
6
- [sqlseed](https://sunbos.github.io/sqlseed/) 的可选 LLM Schema 分析与契约驱动配置修复插件。
6
+ [sqlseed](https://sunbos.github.io/sqlseed/zh-CN/) 的可选 LLM Schema 分析与契约驱动配置修复插件。
7
7
  提供列规则建议、配置校验与修复,以及模板候选值生成。接受的配置可交由 Core 离线执行。
8
8
 
9
9
  支持 Google AI Studio、LM Studio、Ollama 和 OpenAI-compatible API 后端。
@@ -11,13 +11,13 @@
11
11
 
12
12
  ## 安装
13
13
 
14
- 安装 0.2.4 版本时,使用 Python 3.10+ 虚拟环境:
14
+ 本文安装说明对应 0.2.6 版本;发布状态以 [Releases](https://github.com/sunbos/sqlseed/releases) 为准。测试尚未发布的候选版本时,使用下方的源码安装方式。请先创建并激活 Python 3.10+ 虚拟环境:
15
15
 
16
16
  ```bash
17
- python -m pip install "sqlseed-ai==0.2.4"
17
+ python -m pip install "sqlseed-ai==0.2.6"
18
18
  ```
19
19
 
20
- Core 0.2.3 缺少本插件使用的 hooks 与数据库目标校验接口。
20
+ Core 0.2.4 及更早版本缺少本版本使用的共享诊断接口。
21
21
  开发源码时,从仓库根一次安装本地 Core、CLI 和 AI:
22
22
 
23
23
  ```bash
@@ -91,7 +91,7 @@ native/custom 方法、实际生成值及依赖数据库状态的约束需另行
91
91
  AI MCP 入口要求本包的 `mcp` extra:
92
92
 
93
93
  ```bash
94
- python -m pip install "sqlseed-ai[mcp]==0.2.4"
94
+ python -m pip install "sqlseed-ai[mcp]==0.2.6"
95
95
  mcp-server-sqlseed-ai
96
96
  ```
97
97
 
@@ -114,6 +114,9 @@ mcp-server-sqlseed-ai
114
114
  后端解析顺序是显式 `SQLSEED_AI_BACKEND`、已知 URL 模式、最后 `openai_compat`。
115
115
  这不是逐个探测所有服务的 fallback 链。
116
116
 
117
+ AI 请求访问 `localhost` 或回环 IP 地址时直接连接,不经过环境代理;远程服务仍使用
118
+ 原有代理配置。HTTPS 证书校验继续遵循 `SSL_CERT_FILE` 和 `SSL_CERT_DIR`。
119
+
117
120
  | 变量 | 用途 |
118
121
  | --- | --- |
119
122
  | `SQLSEED_AI_BACKEND` | `google_ai_studio`、`lm_studio`、`ollama` 或 `openai_compat` |
@@ -148,6 +151,33 @@ Gemma 26B ID。注册的模型名称不保证服务当前提供该模型,请
148
151
  修正;默认最多重试 3 次,仍失败时报告 `AISuggestionFailedError`。
149
152
  `ai-analyze` 和 `auto-heal` 则使用 v4 `AutoHealOrchestrator` 的契约驱动路径。
150
153
 
154
+ 单表 `ai-suggest` 在请求模型前检查目标表是否存在;建议与缓存必须指向同一张表。
155
+ `--no-verify` 和 `--max-retries 0` 只跳过生成校验,仍保留目标保护。
156
+ SQLite 表名大小写别名继续可用;导出保留真实表名和列名,包括前导 `.` 或 `:`。
157
+ 拒绝的建议不会覆盖已有输出文件。
158
+
159
+ Prompt 明确要求只为指定表返回一个 JSON 对象,并保留表名和列名原文。上下文中的
160
+ 其他表名只供参考,不是额外输出目标;这一提示不能替代响应后的目标校验。
161
+
162
+ 直接分析(`--no-verify` 或 `--max-retries 0`)的流式和非流式路径均分别说明空回答、无效 JSON、输出长度
163
+ 截断和空配置对象,这些诊断不回显模型原文。仅在既定流程中还有更短提示层时才提示
164
+ 并继续重试;最后一层失败会报告具体原因并以失败退出,不再声称正在重试,也不增加
165
+ 请求预算。拒绝响应时,已有输出 YAML 与数据库均保持不变。
166
+
167
+ Python 调用可使用 `SchemaAnalyzer.call_llm(..., strict_json=True)` 或
168
+ `call_llm_streaming(..., strict_json=True)`,通过不含原文的
169
+ `JSONResponseError.code` 区分空回答、无效 JSON 和输出长度截断。解析器可补齐末尾缺失的
170
+ `}` / `]`,包括代码围栏内的 JSON,但不会补值或字符串;达到输出长度上限时,即使前缀
171
+ 可解析也会拒绝,包括流式独立空终止帧中的长度截断标记。两种 Python 方法默认均为
172
+ `strict_json=False`。严格本地调用请求 JSON 采样约束:LM Studio 使用 JSON Schema 语法接口,
173
+ Ollama 使用 JSON 对象模式。服务明确拒绝该格式时,只进行一次文本模式兼容请求,仍由同一
174
+ 严格解析器拒绝无效或截断输出。CLI 直接分析与 refiner 保留各自现有提示与自纠正重试预算。
175
+ 严格工具调用还会拒绝数组、标量和 `null` 参数,
176
+ 返回 `invalid_json`;兼容模式仍可回退到响应正文。解析后的建议仍需验证范围和业务规则。
177
+
178
+ 直接 Python 调用可给 `SchemaAnalyzer.call_llm()` 或 `call_llm_streaming()` 传入
179
+ `preserve_names=True`,保留标识符后再按实际 schema 校验;默认仍保留既有的前导标点清理。
180
+
151
181
  开启 AI 生成路径时,`sqlseed_pre_generate_templates` 可为符合条件的未匹配字符串列
152
182
  准备候选值。用户明确配置、UNIQUE、默认值或主键等条件会影响是否使用模板池,
153
183
  不保证每个复杂字段都会调用模型。
@@ -164,7 +194,11 @@ Gemma 26B ID。注册的模型名称不保证服务当前提供该模型,请
164
194
 
165
195
  ### 文件缓存
166
196
 
167
- AI 配置缓存包含 schema hash,结构变化会使旧建议失效;`--no-cache` 跳过缓存。
197
+ 单表建议缓存使用带版本标识的 schema hash,将排序后的列名编码为 JSON 数组,
198
+ 避免含分隔符的不同列名集合混淆。校验覆盖列的增删和重命名,不包含列顺序、类型或约束。
199
+ 旧分隔符编码的缓存会被忽略并重新生成;可用 `--no-cache` 跳过缓存重新分析。
200
+ 此处的缓存校验与 AutoHeal 使用的完整 schema 指纹不同。
201
+ 缓存元数据或配置容器类型无效时,按缓存未命中重新分析。
168
202
  默认路径为 macOS 的 `~/Library/Caches/sqlseed/ai_configs/`、Linux 的
169
203
  `$XDG_CACHE_HOME/sqlseed/ai_configs/`(未设置时为 `~/.cache/sqlseed/ai_configs/`),
170
204
  以及 Windows 的 `%LOCALAPPDATA%/sqlseed/ai_configs/`。
@@ -184,19 +218,32 @@ AI 配置缓存包含 schema hash,结构变化会使旧建议失效;`--no-ca
184
218
  CLI 命令另由 `sqlseed.cli_commands` entry point 注册。本插件不实现 provider 或
185
219
  column-mapper 注册 hooks,也不要求 Core 导入 AI 实现。
186
220
 
221
+ ## macOS 硬件估算
222
+
223
+ AI MCP 模型列表区分 Apple 统一内存、Intel 共享显存与独立显存。Apple 统一内存
224
+ 不会被报告为独立显存,也不会与系统 RAM 相加。`unified_memory_budget_gb` 是静态
225
+ 启发式预算:`max(0, min(total_ram_gb * 0.75, total_ram_gb - 4))`,为系统预留
226
+ 至少 4 GiB 或 25% 内存。它不是实测可用内存,也不是 Metal 分配上限。
227
+
228
+ 仅在 macOS 上识别出 Apple GPU 后,模型筛选才使用该预算与既有最低 RAM、VRAM
229
+ 估算同时比较,最高返回 `capable`。该结果不代表已验证 Metal 加速、后端或模型支持,
230
+ 也不保证推理成功;其他应用占用和上下文大小可能增加实际内存需求。
231
+
187
232
  ## 依赖
188
233
 
234
+ 以下为 0.2.6 版本及其源码候选的依赖要求。源码开发时请在同一次解析中安装本地 Core 和插件。
235
+
189
236
  - Python `>=3.10`
190
- - `sqlseed>=0.2.4.dev0,<0.3`
237
+ - `sqlseed>=0.2.5.dev0,<0.3`
191
238
  - `sqlseed-cli>=0.2.4.dev0,<0.3`
192
- - `openai>=1.0`
239
+ - `openai>=1.55.3`(保留 SDK 传输默认值并兼容 HTTPX 0.28)
193
240
  - `httpx>=0.24.0`
194
241
  - `networkx>=3.0`
195
242
  - 可选 `mcp` extra:`mcp>=1.0,<2`
196
243
  - 实际模型请求需要已配置且可达的后端
197
244
 
198
- 更多信息见[AI 集成指南](https://sunbos.github.io/sqlseed/gemma4-integration.zh-CN/)、
199
- [升级说明](https://sunbos.github.io/sqlseed/migration.zh-CN/)和
245
+ 更多信息见[AI 集成指南](https://sunbos.github.io/sqlseed/zh-CN/gemma4-integration/)、
246
+ [升级说明](https://sunbos.github.io/sqlseed/zh-CN/migration/)和
200
247
  [配置源码](https://github.com/sunbos/sqlseed/blob/main/plugins/sqlseed-ai/src/sqlseed_ai/config.py)。
201
248
 
202
249
  许可证:[AGPL-3.0-or-later](https://github.com/sunbos/sqlseed/blob/main/LICENSE)。
@@ -23,9 +23,9 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.13",
24
24
  ]
25
25
  dependencies = [
26
- "sqlseed>=0.2.4.dev0,<0.3",
26
+ "sqlseed>=0.2.5.dev0,<0.3",
27
27
  "sqlseed-cli>=0.2.4.dev0,<0.3",
28
- "openai>=1.0",
28
+ "openai>=1.55.3",
29
29
  "httpx>=0.24.0",
30
30
  "networkx>=3.0",
31
31
  ]
@@ -7,18 +7,25 @@ unified httpx timeouts suitable for both cloud and local (GPU) inference.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from ipaddress import ip_address
11
+ from typing import TYPE_CHECKING
12
+
10
13
  import httpx
11
- from openai import APIConnectionError, APIError, APITimeoutError, OpenAI
14
+ from openai import APIConnectionError, APIError, APITimeoutError
12
15
  from sqlseed_ai.config import AIConfig
13
16
 
14
17
  from sqlseed._utils.logger import get_logger
15
18
 
19
+ if TYPE_CHECKING:
20
+ from openai import OpenAI
21
+
16
22
  logger = get_logger(__name__)
17
23
 
18
24
  __all__ = [
19
25
  "APIConnectionError",
20
26
  "APIError",
21
27
  "APITimeoutError",
28
+ "build_openai_client",
22
29
  "get_openai_client",
23
30
  "httpx_timeout",
24
31
  ]
@@ -49,7 +56,36 @@ def get_openai_client(config: AIConfig | None = None) -> OpenAI:
49
56
  # - pool=10s: connection-pool acquisition timeout
50
57
  kwargs["timeout"] = httpx_timeout(config.resolve_timeout())
51
58
  logger.info("Creating OpenAI client", **{"backend": config.backend.value, "base_url": kwargs["base_url"]})
52
- return OpenAI(**kwargs)
59
+ return build_openai_client(**kwargs)
60
+
61
+
62
+ def build_openai_client(*, api_key: str, base_url: str, timeout: float | httpx.Timeout | None) -> OpenAI:
63
+ """Create a client with direct loopback routing and SDK transport defaults.
64
+
65
+ Exact loopback hosts bypass environment proxies. TLS certificate settings,
66
+ remote proxy routes, timeouts, redirects, and pool limits remain unchanged.
67
+ The returned SDK client owns its optional HTTP client.
68
+ """
69
+ from openai import DefaultHttpxClient, OpenAI, Timeout
70
+
71
+ resolved_timeout = Timeout(**timeout.as_dict()) if isinstance(timeout, httpx.Timeout) else timeout
72
+ if (host := httpx.URL(base_url).host) != "localhost":
73
+ try:
74
+ is_loopback = ip_address(host).is_loopback
75
+ except ValueError:
76
+ is_loopback = False
77
+ else:
78
+ is_loopback = True
79
+ if not is_loopback:
80
+ return OpenAI(api_key=api_key, base_url=base_url, timeout=resolved_timeout)
81
+
82
+ authority = f"[{host}]" if ":" in host else host
83
+ transport = DefaultHttpxClient(mounts={f"all://{authority}": None})
84
+ try:
85
+ return OpenAI(api_key=api_key, base_url=base_url, timeout=resolved_timeout, http_client=transport)
86
+ except BaseException:
87
+ transport.close()
88
+ raise
53
89
 
54
90
 
55
91
  def httpx_timeout(total: float) -> httpx.Timeout:
@@ -11,8 +11,10 @@ from __future__ import annotations
11
11
  import ctypes
12
12
  import json
13
13
  import platform
14
+ import re
14
15
  import subprocess
15
16
  import time
17
+ from math import isfinite
16
18
  from typing import Any, NamedTuple
17
19
 
18
20
  from sqlseed._utils.logger import get_logger
@@ -200,8 +202,34 @@ def _detect_gpu_nvidia() -> list[dict[str, Any]]:
200
202
  return []
201
203
 
202
204
 
205
+ def _video_memory_mb(value: object) -> int:
206
+ """Parse a profiler memory quantity without trusting malformed card fields."""
207
+ if not isinstance(value, str):
208
+ return 0
209
+ if (match := re.fullmatch(r"(\d+(?:\.\d+)?)\s*(MB|GB)", value.strip(), re.IGNORECASE)) is None:
210
+ return 0
211
+ size_mb = float(match[1]) * (1024 if match[2].upper() == "GB" else 1)
212
+ return int(size_mb) if isfinite(size_mb) else 0
213
+
214
+
215
+ def _macos_gpu_memory(gpu_info: dict[str, object], vendor: str) -> tuple[str, int]:
216
+ """Separate shared and dedicated video memory from Apple unified RAM."""
217
+ if vendor == "apple":
218
+ return "unified", 0
219
+ memory_type = "shared" if "spdisplays_vram_shared" in gpu_info else "dedicated"
220
+ vram_mb = next(
221
+ (
222
+ size
223
+ for key in ("spdisplays_vram", "spdisplays_vram_shared", "_spdisplays_vram")
224
+ if (size := _video_memory_mb(gpu_info.get(key))) > 0
225
+ ),
226
+ 0,
227
+ )
228
+ return memory_type, vram_mb
229
+
230
+
203
231
  def _detect_gpu_macos() -> list[dict[str, Any]]:
204
- """Detect Apple Silicon GPU via system_profiler. macOS only."""
232
+ """Detect Apple, Intel and discrete GPUs via macOS system_profiler."""
205
233
  try:
206
234
  result = subprocess.run(
207
235
  ["system_profiler", "SPDisplaysDataType", "-json"],
@@ -214,26 +242,30 @@ def _detect_gpu_macos() -> list[dict[str, Any]]:
214
242
  return []
215
243
 
216
244
  data = json.loads(result.stdout)
245
+ if not isinstance(data, dict):
246
+ return []
217
247
  displays = data.get("SPDisplaysDataType", [])
248
+ if not isinstance(displays, list):
249
+ return []
218
250
  gpus: list[dict[str, Any]] = []
219
251
  for gpu_info in displays:
252
+ if not isinstance(gpu_info, dict):
253
+ continue
220
254
  name = gpu_info.get("sppci_model", "Unknown GPU")
221
- vram_str = gpu_info.get("spdisplays_vram", "")
222
- vram_mb = 0
223
- if vram_str:
224
- parts = vram_str.split()
225
- if len(parts) >= 2:
226
- val = int(parts[0])
227
- unit = parts[1].upper()
228
- vram_mb = val * 1024 if "GB" in unit else val
255
+ if not isinstance(name, str):
256
+ name = "Unknown GPU"
257
+ vendor_label = str(gpu_info.get("spdisplays_vendor") or name).lower()
258
+ vendor = next((v for v in ("apple", "intel", "amd", "nvidia") if v in vendor_label), "unknown")
259
+ memory_type, vram_mb = _macos_gpu_memory(gpu_info, vendor)
229
260
 
230
261
  gpus.append(
231
262
  {
232
263
  "name": name,
233
264
  "vram_total_mb": vram_mb,
234
- "vram_free_mb": 0, # Apple Silicon uses unified memory; discrete VRAM is always 0
265
+ "vram_free_mb": 0, # system_profiler does not report free VRAM
235
266
  "vram_total_gb": round(vram_mb / 1024, 1),
236
- "vendor": "apple",
267
+ "vendor": vendor,
268
+ "memory_type": memory_type,
237
269
  }
238
270
  )
239
271
  return gpus
@@ -252,6 +284,13 @@ def _detect_gpus() -> list[dict[str, Any]]:
252
284
  return []
253
285
 
254
286
 
287
+ def _has_apple_unified_memory(hw: dict[str, Any]) -> bool:
288
+ """Require an identified Apple GPU on macOS, including under Rosetta."""
289
+ return hw.get("platform", {}).get("system") == "Darwin" and any(
290
+ gpu.get("vendor") == "apple" and gpu.get("memory_type") == "unified" for gpu in hw.get("gpus", [])
291
+ )
292
+
293
+
255
294
  # ── Public API ───────────────────────────────────────────────────────
256
295
 
257
296
 
@@ -262,7 +301,8 @@ def detect_hardware() -> dict[str, Any]:
262
301
  platform: {system, release, machine}
263
302
  ram: {total_gb, available_gb}
264
303
  gpus: [{name, vram_total_mb, vram_free_mb, vram_total_gb, vendor, ...}]
265
- max_vram_gb: float (max VRAM across all GPUs, 0 if no GPU)
304
+ max_vram_gb: float (largest reported non-unified VRAM quantity)
305
+ unified_memory_budget_gb: float (heuristic budget, not measured free VRAM)
266
306
  """
267
307
  if _HardwareCache.data is not None:
268
308
  cached_time, cached_result = _HardwareCache.data
@@ -271,9 +311,9 @@ def detect_hardware() -> dict[str, Any]:
271
311
 
272
312
  ram = _detect_system_ram()
273
313
  gpus = _detect_gpus()
274
- max_vram = max((g.get("vram_total_gb", 0) for g in gpus), default=0)
314
+ max_vram = max((g.get("vram_total_gb", 0) for g in gpus if g.get("memory_type") != "unified"), default=0)
275
315
 
276
- result = {
316
+ result: dict[str, Any] = {
277
317
  "platform": {
278
318
  "system": platform.system(),
279
319
  "release": platform.release(),
@@ -282,7 +322,13 @@ def detect_hardware() -> dict[str, Any]:
282
322
  "ram": ram,
283
323
  "gpus": gpus,
284
324
  "max_vram_gb": max_vram,
325
+ "unified_memory_budget_gb": 0.0,
285
326
  }
327
+ if _has_apple_unified_memory(result):
328
+ # A static screening heuristic, not Metal's runtime allocation limit.
329
+ # Reserve at least 4 GiB / 25% for the OS and other applications.
330
+ total_ram = ram["total_gb"]
331
+ result["unified_memory_budget_gb"] = max(0.0, min(total_ram * 0.75, total_ram - 4.0))
286
332
 
287
333
  _HardwareCache.data = (time.monotonic(), result)
288
334
  logger.info(
@@ -340,6 +386,11 @@ def evaluate_model_status(
340
386
  return "recommended"
341
387
  if max_vram >= req.min_vram_gb:
342
388
  return "capable"
389
+ if _has_apple_unified_memory(hw):
390
+ budget = hw.get("unified_memory_budget_gb", 0)
391
+ # RAM and GPU allocations share this budget; never add them or grant
392
+ # a recommendation without verifying the backend and actual model.
393
+ return "capable" if budget >= max(req.min_ram_gb, req.min_vram_gb) else "insufficient"
343
394
  if total_ram >= req.min_ram_gb and max_vram == 0:
344
395
  return "cpu_only"
345
396
  if total_ram >= req.min_ram_gb: