ecadlib 0.3.0__py3-none-any.whl
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.
- ecadlib/__init__.py +3 -0
- ecadlib/__main__.py +5 -0
- ecadlib/a365_sync.py +637 -0
- ecadlib/agent.py +1407 -0
- ecadlib/agent_queue.py +780 -0
- ecadlib/agent_watch.py +732 -0
- ecadlib/altium365.py +385 -0
- ecadlib/altium_dblib_sync.py +530 -0
- ecadlib/altium_export.py +209 -0
- ecadlib/altium_forms.py +768 -0
- ecadlib/altium_lib.py +1089 -0
- ecadlib/altium_menu.py +1751 -0
- ecadlib/altium_pkg.py +396 -0
- ecadlib/altium_project.py +1095 -0
- ecadlib/app.py +1458 -0
- ecadlib/approvals.py +395 -0
- ecadlib/assets/demo_bom.csv +8 -0
- ecadlib/assets/kicad_action_plugin.py +607 -0
- ecadlib/assets/kicad_reread.py +60 -0
- ecadlib/assets/pcblib_template.json.gz +0 -0
- ecadlib/bom_inputs.py +339 -0
- ecadlib/bom_triage.py +519 -0
- ecadlib/cad_model.py +528 -0
- ecadlib/cfb.py +171 -0
- ecadlib/cli.py +185 -0
- ecadlib/clihelp.py +149 -0
- ecadlib/cliutil.py +119 -0
- ecadlib/completion.py +173 -0
- ecadlib/costs.py +276 -0
- ecadlib/crosscheck.py +412 -0
- ecadlib/datasheet.py +2168 -0
- ecadlib/dedupe.py +572 -0
- ecadlib/detect.py +170 -0
- ecadlib/doctor.py +450 -0
- ecadlib/ds_route.py +541 -0
- ecadlib/eagle_export.py +1409 -0
- ecadlib/easy_setup.py +338 -0
- ecadlib/easyeda_convert.py +386 -0
- ecadlib/easyeda_pro_export.py +972 -0
- ecadlib/eval/__init__.py +0 -0
- ecadlib/eval/baselines/kicad-v1.json +404 -0
- ecadlib/eval/build_set.py +299 -0
- ecadlib/eval/cli.py +119 -0
- ecadlib/eval/gate.py +358 -0
- ecadlib/eval/golden/demo.txt +379 -0
- ecadlib/eval/golden/specs.txt +3120 -0
- ecadlib/eval/golden/testpack.txt +463 -0
- ecadlib/eval/golden.py +446 -0
- ecadlib/eval/replay/kicad-v1.json.gz +0 -0
- ecadlib/eval/runner.py +456 -0
- ecadlib/eval/score.py +179 -0
- ecadlib/eval/sets/kicad-v1.json +630 -0
- ecadlib/firstrun.py +400 -0
- ecadlib/footprint_adv.py +1006 -0
- ecadlib/footprint_bench.py +218 -0
- ecadlib/footprint_check.py +562 -0
- ecadlib/footprint_gen.py +582 -0
- ecadlib/footprint_rules.py +1092 -0
- ecadlib/forge.py +267 -0
- ecadlib/fp_share.py +95 -0
- ecadlib/fp_tidy.py +389 -0
- ecadlib/fsutil.py +62 -0
- ecadlib/house_style.py +1051 -0
- ecadlib/httpcache.py +124 -0
- ecadlib/hub.py +1490 -0
- ecadlib/hub_copy.py +161 -0
- ecadlib/hub_tools.py +79 -0
- ecadlib/kicad_dblib.py +257 -0
- ecadlib/kicad_export.py +837 -0
- ecadlib/kicad_import.py +913 -0
- ecadlib/kicad_index.py +160 -0
- ecadlib/kicad_menu.py +193 -0
- ecadlib/kicad_pcm.py +475 -0
- ecadlib/launcher.py +156 -0
- ecadlib/librarian.py +963 -0
- ecadlib/lifecycle.py +691 -0
- ecadlib/llm/__init__.py +276 -0
- ecadlib/llm/cli.py +70 -0
- ecadlib/llm/openai_compat.py +214 -0
- ecadlib/llm/profiles.py +245 -0
- ecadlib/llm/rails.py +570 -0
- ecadlib/llm/run.py +126 -0
- ecadlib/llm/sampling.py +49 -0
- ecadlib/mcp_server.py +723 -0
- ecadlib/messages.py +419 -0
- ecadlib/migrate.py +407 -0
- ecadlib/notify.py +329 -0
- ecadlib/package_schema.json +1238 -0
- ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
- ecadlib/package_specs/C_0402_1005Metric.json +22 -0
- ecadlib/package_specs/C_0805_2012Metric.json +22 -0
- ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
- ecadlib/package_specs/D_SMA.json +27 -0
- ecadlib/package_specs/D_SMB.json +27 -0
- ecadlib/package_specs/D_SOD-123.json +43 -0
- ecadlib/package_specs/D_SOD-323.json +43 -0
- ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
- ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
- ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
- ecadlib/package_specs/L_0805_2012Metric.json +22 -0
- ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
- ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
- ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
- ecadlib/package_specs/R_0402_1005Metric.json +22 -0
- ecadlib/package_specs/R_0603_1608Metric.json +22 -0
- ecadlib/package_specs/R_0805_2012Metric.json +22 -0
- ecadlib/package_specs/R_1206_3216Metric.json +22 -0
- ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
- ecadlib/package_specs/SOT-23-5.json +57 -0
- ecadlib/package_specs/SOT-23-6.json +62 -0
- ecadlib/package_specs/SOT-23.json +48 -0
- ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
- ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
- ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
- ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
- ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
- ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
- ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
- ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
- ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
- ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
- ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
- ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
- ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
- ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
- ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
- ecadlib/package_specs/standard.json.gz +0 -0
- ecadlib/parity.py +1204 -0
- ecadlib/parts_export.py +154 -0
- ecadlib/partview.py +215 -0
- ecadlib/plugins.py +231 -0
- ecadlib/providers.py +226 -0
- ecadlib/rebuild.py +471 -0
- ecadlib/release_check.py +437 -0
- ecadlib/reports.py +274 -0
- ecadlib/resolve.py +556 -0
- ecadlib/review_cli.py +292 -0
- ecadlib/rules.py +979 -0
- ecadlib/rules_infer.py +1205 -0
- ecadlib/sexpr.py +149 -0
- ecadlib/sheetsafe.py +65 -0
- ecadlib/sim_models.py +1087 -0
- ecadlib/sourcing.py +1161 -0
- ecadlib/stdparts.py +755 -0
- ecadlib/step_gen.py +929 -0
- ecadlib/symbol_layout.py +798 -0
- ecadlib/team.py +558 -0
- ecadlib/team_setup.py +288 -0
- ecadlib/term.py +420 -0
- ecadlib/term_views.py +64 -0
- ecadlib/testpack.py +334 -0
- ecadlib/ui/__init__.py +1 -0
- ecadlib/ui/__main__.py +3 -0
- ecadlib/ui/server.py +1029 -0
- ecadlib/ui/static/hairline.js +2 -0
- ecadlib/ui/static/index.html +2098 -0
- ecadlib/ui/static/setup.html +294 -0
- ecadlib/upkeep.py +717 -0
- ecadlib/upstream.py +573 -0
- ecadlib/userconfig.py +257 -0
- ecadlib/visual_check.py +1603 -0
- ecadlib/ws_status.py +203 -0
- ecadlib-0.3.0.dist-info/METADATA +2245 -0
- ecadlib-0.3.0.dist-info/RECORD +199 -0
- ecadlib-0.3.0.dist-info/WHEEL +5 -0
- ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
- ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
- ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
- ecadlib-0.3.0.dist-info/top_level.txt +1 -0
ecadlib/llm/profiles.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""What a model can do, and how much help it gets: capability profiles.
|
|
2
|
+
|
|
3
|
+
A profile is {tier, pdf, vision, json_schema, context, max_tokens_param, price_in, price_out, source}:
|
|
4
|
+
tier frontier | mid | small - picks the guide rails with [ai] assist = "auto" (ecadlib.llm.rails)
|
|
5
|
+
pdf the endpoint takes a PDF as input (Claude document blocks; OpenAI file parts)
|
|
6
|
+
vision it takes images (page pictures when the PDF is sent as text; visual-check needs it)
|
|
7
|
+
json_schema it honours a JSON-schema response format (else: schema in the prompt + JSON repair)
|
|
8
|
+
context context window in tokens (sizes how much page text is sent)
|
|
9
|
+
max_tokens_param max_tokens | max_completion_tokens (OpenAI's newer models want the second)
|
|
10
|
+
price_in / price_out $ per million tokens when known (Claude: datasheet.PRICES; a local server is $0)
|
|
11
|
+
sampling it takes a temperature (unset: ecadlib.llm.sampling's table of models that reject one)
|
|
12
|
+
|
|
13
|
+
Where it comes from, later wins: CONSERVATIVE (an unknown model: small, text only, no schema, 4K context) ->
|
|
14
|
+
the built-in table below (best knowledge as of 2026-10; names matched without a vendor prefix such as
|
|
15
|
+
"openai/" or "anthropic/") -> what the endpoint reports (detect(): /models on OpenRouter, vLLM, LM Studio;
|
|
16
|
+
/api/show on Ollama) -> [ai.profiles."<model>"] in ecadlib.toml (always wins)."""
|
|
17
|
+
import json, re
|
|
18
|
+
|
|
19
|
+
TIERS = ("frontier", "mid", "small")
|
|
20
|
+
FIELDS = {"tier": str, "pdf": bool, "vision": bool, "json_schema": bool, "context": int, "max_tokens_param": str,
|
|
21
|
+
"price_in": float, "price_out": float, "sampling": bool}
|
|
22
|
+
CONSERVATIVE = {"tier": "small", "pdf": False, "vision": False, "json_schema": False, "context": 4096,
|
|
23
|
+
"max_tokens_param": "max_tokens", "price_in": None, "price_out": None}
|
|
24
|
+
_C = dict(tier="frontier", pdf=True, vision=True, json_schema=True, context=1_000_000)
|
|
25
|
+
# (regex on the bare model name, profile fields). First match wins. Tiers are judgement calls for THIS task (reading a
|
|
26
|
+
# package drawing and a pin table exactly); override any of them with [ai.profiles."<model>"].
|
|
27
|
+
BUILTIN = [
|
|
28
|
+
(r"claude-(haiku|sonnet|opus)-4-5", dict(_C, context=200_000)),
|
|
29
|
+
(r"claude-.*", _C),
|
|
30
|
+
# OpenAI's 2026 names (context and PDF input assumed as for gpt-5; correct with [ai.profiles] if wrong)
|
|
31
|
+
(r"gpt-[56](\.\d+)?-luna.*", dict(tier="small", pdf=True, vision=True, json_schema=True, context=400_000,
|
|
32
|
+
max_tokens_param="max_completion_tokens")),
|
|
33
|
+
(r"gpt-[56](\.\d+)?-terra.*", dict(tier="mid", pdf=True, vision=True, json_schema=True, context=400_000,
|
|
34
|
+
max_tokens_param="max_completion_tokens")),
|
|
35
|
+
(r"gpt-6.*|gpt-5(\.\d+)?-sol.*", dict(tier="frontier", pdf=True, vision=True, json_schema=True, context=400_000,
|
|
36
|
+
max_tokens_param="max_completion_tokens")),
|
|
37
|
+
(r"gpt-5(\.\d+)?-nano.*", dict(tier="small", pdf=True, vision=True, json_schema=True, context=400_000,
|
|
38
|
+
max_tokens_param="max_completion_tokens")),
|
|
39
|
+
(r"gpt-5(\.\d+)?-mini.*", dict(tier="mid", pdf=True, vision=True, json_schema=True, context=400_000,
|
|
40
|
+
max_tokens_param="max_completion_tokens")),
|
|
41
|
+
(r"gpt-5.*", dict(tier="frontier", pdf=True, vision=True, json_schema=True, context=400_000,
|
|
42
|
+
max_tokens_param="max_completion_tokens")),
|
|
43
|
+
(r"o[34](-pro)?(-\d.*)?", dict(tier="frontier", pdf=True, vision=True, json_schema=True, context=200_000,
|
|
44
|
+
max_tokens_param="max_completion_tokens")),
|
|
45
|
+
(r"o4-mini.*", dict(tier="mid", pdf=True, vision=True, json_schema=True, context=200_000,
|
|
46
|
+
max_tokens_param="max_completion_tokens")),
|
|
47
|
+
(r"gpt-4\.1-nano.*|gpt-4o-mini.*", dict(tier="small", pdf=True, vision=True, json_schema=True, context=128_000)),
|
|
48
|
+
(r"gpt-4\.1.*|gpt-4o.*", dict(tier="mid", pdf=True, vision=True, json_schema=True, context=128_000)),
|
|
49
|
+
(r"gemini-[\d.]+-(pro|argon).*", dict(tier="frontier", pdf=False, vision=True, json_schema=True, context=1_000_000)),
|
|
50
|
+
(r"gemini-[\d.]+-flash-lite.*", dict(tier="small", pdf=False, vision=True, json_schema=True, context=1_000_000)),
|
|
51
|
+
(r"gemini-.*flash.*", dict(tier="mid", pdf=False, vision=True, json_schema=True, context=1_000_000)),
|
|
52
|
+
(r"deepseek-(chat|reasoner|v3.*|r1)", dict(tier="mid", pdf=False, vision=False, json_schema=False, context=64_000)),
|
|
53
|
+
(r"deepseek-flash.*", dict(tier="mid", pdf=False, vision=True, json_schema=False, context=64_000)),
|
|
54
|
+
(r"deepseek-v4.*", dict(tier="mid", pdf=False, vision=False, json_schema=False, context=64_000)),
|
|
55
|
+
]
|
|
56
|
+
# List prices, $ per million tokens (input, output), standard tier, prompts up to 200K, for models called directly
|
|
57
|
+
# (not through OpenRouter, whose /models reports its own). Checked 2026-10-16 against benchlm.ai and promptunit.ai
|
|
58
|
+
# (third-party price trackers); vendors change these, so [ai.profiles."<model>"] price_in / price_out wins.
|
|
59
|
+
# Claude's prices are datasheet.PRICES. Known changes: gemini-4-argon $4/$20 from 2026-12-01, gemini-3.8-flash
|
|
60
|
+
# $1.50/$7.50 from 2027-01-01; DeepSeek bills half these rates off-peak.
|
|
61
|
+
PRICES = {
|
|
62
|
+
"gpt-6-astra": (10.0, 50.0), "gpt-6.1-sol": (2.0, 10.0), "gpt-6-sol": (2.0, 10.0), "gpt-6-luna": (0.10, 0.50),
|
|
63
|
+
"gpt-5.6-terra": (2.0, 12.0), "gpt-5.6-luna": (0.20, 1.20), "gpt-5.5": (5.0, 30.0), "gpt-5.4": (2.50, 15.0),
|
|
64
|
+
"gpt-5.4-mini": (0.75, 4.50), "gpt-5.4-nano": (0.20, 1.25), "gpt-5.2": (1.75, 14.0), "gpt-5.1": (1.25, 10.0),
|
|
65
|
+
"gpt-5": (1.25, 10.0), "gpt-5-mini": (0.25, 2.0), "gpt-5-nano": (0.05, 0.40),
|
|
66
|
+
"gemini-4-argon": (2.0, 10.0), "gemini-3.1-pro-preview": (2.0, 12.0), "gemini-3.8-flash": (0.75, 3.75),
|
|
67
|
+
"gemini-3.5-flash": (1.50, 9.0), "gemini-3-flash-preview": (0.50, 3.0), "gemini-3.1-flash-lite": (0.25, 1.50),
|
|
68
|
+
"gemini-2.5-pro": (1.25, 10.0), "gemini-2.5-flash": (0.30, 2.50), "gemini-2.5-flash-lite": (0.10, 0.40),
|
|
69
|
+
"deepseek-v4-pro": (1.32, 3.96), "deepseek-flash": (0.30, 1.20)}
|
|
70
|
+
VISION_NAME = re.compile(r"vl\b|vl[-:]|vision|llava|pixtral|minicpm-v|gemma-?3|qwen2\.5-?vl|llama-?4|moondream|-v\b", re.I)
|
|
71
|
+
LOCAL_FAMILY = re.compile(r"llama|llava|pixtral|minicpm|moondream|qwen|mistral|mixtral|gemma|phi|deepseek|granite|command-r|olmo|smollm|nemotron|yi|glm", re.I)
|
|
72
|
+
SIZE = re.compile(r"(?<![\d.])(\d+(?:\.\d+)?)\s*b\b", re.I)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def bare(model):
|
|
76
|
+
"""'openai/gpt-5' -> 'gpt-5', 'anthropic/claude-opus-5-5' -> 'claude-opus-5-5', 'local:qwen2.5:7b' handled by the
|
|
77
|
+
caller (ecadlib.llm.split_ref); 'hf.co/x/y:Q4' -> 'y:q4'. Lower case."""
|
|
78
|
+
return str(model or "").rsplit("/", 1)[-1].lower()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def builtin(model):
|
|
82
|
+
"""-> (fields, source) from the table, or a guess for local open-weight families by size, or ({}, None)."""
|
|
83
|
+
m = bare(model)
|
|
84
|
+
for rx, prof in BUILTIN:
|
|
85
|
+
if re.fullmatch(rx, m):
|
|
86
|
+
got = dict(prof)
|
|
87
|
+
if m in PRICES:
|
|
88
|
+
got["price_in"], got["price_out"] = PRICES[m]
|
|
89
|
+
return got, f"built-in ({rx})"
|
|
90
|
+
if LOCAL_FAMILY.search(m):
|
|
91
|
+
sz = SIZE.search(m.replace(":", " ").replace("-", " "))
|
|
92
|
+
b = float(sz.group(1)) if sz else None
|
|
93
|
+
tier = "mid" if b and b >= 60 else "small"
|
|
94
|
+
return ({"tier": tier, "vision": bool(VISION_NAME.search(m)), "json_schema": True, "pdf": False,
|
|
95
|
+
"context": 8192}, f"built-in (open-weight family, {('%gB' % b) if b else 'size unknown'} -> {tier})")
|
|
96
|
+
return {}, None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def profile(model, provider="anthropic", rules=None, detected=None):
|
|
100
|
+
"""The profile for `model` (a bare model id, without an endpoint prefix)."""
|
|
101
|
+
p, src = dict(CONSERVATIVE), ["default (unknown model: conservative)"]
|
|
102
|
+
got, s = builtin(model)
|
|
103
|
+
if provider == "anthropic" and not got: # any Claude id the table does not know yet
|
|
104
|
+
got, s = dict(_C), "built-in (anthropic)"
|
|
105
|
+
if got:
|
|
106
|
+
p.update(got)
|
|
107
|
+
src = [s]
|
|
108
|
+
if detected:
|
|
109
|
+
p.update({k: v for k, v in detected.items() if k in FIELDS and v is not None})
|
|
110
|
+
src.append("detected: " + ", ".join(sorted(k for k in detected if k in FIELDS and detected[k] is not None)))
|
|
111
|
+
over = (((rules or {}).get("ai") or {}).get("profiles") or {}) if isinstance(rules, dict) else {}
|
|
112
|
+
o = over.get(model) or over.get(bare(model)) or {}
|
|
113
|
+
if o:
|
|
114
|
+
p.update({k: v for k, v in o.items() if k in FIELDS})
|
|
115
|
+
src.append("ecadlib.toml [ai.profiles] (" + ", ".join(sorted(o)) + ")")
|
|
116
|
+
p["source"] = "; ".join(src)
|
|
117
|
+
return p
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def check_field(k, v):
|
|
121
|
+
"""Validation message for one [ai.profiles] key, or None."""
|
|
122
|
+
if k not in FIELDS:
|
|
123
|
+
return f"is not a known key; known: {', '.join(FIELDS)}"
|
|
124
|
+
if k == "tier" and v not in TIERS:
|
|
125
|
+
return f"= {v!r}: must be frontier, mid or small"
|
|
126
|
+
if k == "max_tokens_param" and v not in ("max_tokens", "max_completion_tokens"):
|
|
127
|
+
return "must be max_tokens or max_completion_tokens"
|
|
128
|
+
t = FIELDS[k]
|
|
129
|
+
if t is bool and not isinstance(v, bool):
|
|
130
|
+
return "must be true or false"
|
|
131
|
+
if t is int and (not isinstance(v, int) or isinstance(v, bool) or not 512 <= v <= 10_000_000):
|
|
132
|
+
return "must be a whole number of tokens from 512 to 10000000"
|
|
133
|
+
if t is float and (not isinstance(v, (int, float)) or isinstance(v, bool) or v < 0):
|
|
134
|
+
return "must be a number of 0 or more ($ per million tokens)"
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------- cheap auto-detection (openai-compatible servers)
|
|
139
|
+
_DETECTED = {}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _num(v):
|
|
143
|
+
try:
|
|
144
|
+
return float(v)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def from_models_entry(e):
|
|
150
|
+
"""One entry of GET /models (OpenRouter, vLLM, LM Studio's /api/v0/models, ...) -> profile fields it states."""
|
|
151
|
+
out = {}
|
|
152
|
+
ctx = e.get("context_length") or e.get("max_model_len") or e.get("max_context_length") or \
|
|
153
|
+
(e.get("top_provider") or {}).get("context_length")
|
|
154
|
+
if isinstance(ctx, (int, float)) and ctx > 0:
|
|
155
|
+
out["context"] = int(ctx)
|
|
156
|
+
mods = (e.get("architecture") or {}).get("input_modalities")
|
|
157
|
+
if isinstance(mods, list):
|
|
158
|
+
out["vision"] = "image" in mods
|
|
159
|
+
out["pdf"] = "file" in mods
|
|
160
|
+
if e.get("type") in ("vlm", "llm"): # LM Studio
|
|
161
|
+
out["vision"] = e["type"] == "vlm"
|
|
162
|
+
sp = e.get("supported_parameters")
|
|
163
|
+
if isinstance(sp, list):
|
|
164
|
+
out["json_schema"] = "structured_outputs" in sp or "response_format" in sp
|
|
165
|
+
pr = e.get("pricing") or {}
|
|
166
|
+
pin, pout = _num(pr.get("prompt")), _num(pr.get("completion"))
|
|
167
|
+
if pin is not None and pout is not None and pin >= 0 and pout >= 0:
|
|
168
|
+
out["price_in"], out["price_out"] = round(pin * 1e6, 6), round(pout * 1e6, 6)
|
|
169
|
+
return out
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def from_ollama_show(j):
|
|
173
|
+
"""POST /api/show -> profile fields: capabilities ['completion', 'vision', ...], model_info *.context_length."""
|
|
174
|
+
out = {}
|
|
175
|
+
caps = j.get("capabilities")
|
|
176
|
+
if isinstance(caps, list):
|
|
177
|
+
out["vision"] = "vision" in caps
|
|
178
|
+
for k, v in (j.get("model_info") or {}).items():
|
|
179
|
+
if k.endswith(".context_length") and isinstance(v, int) and v > 0:
|
|
180
|
+
out["context"] = v
|
|
181
|
+
return out
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def detect(ep, key=None, get=None, post=None, timeout=5):
|
|
185
|
+
"""Ask the endpoint about the model (cached per base_url + model). Never raises: -> (fields, note)."""
|
|
186
|
+
ck = (ep.get("base_url"), ep.get("model"))
|
|
187
|
+
if ck in _DETECTED:
|
|
188
|
+
return _DETECTED[ck]
|
|
189
|
+
import requests
|
|
190
|
+
get, post = get or requests.get, post or requests.post
|
|
191
|
+
h = {"Authorization": f"Bearer {key}"} if key and key != "no-key-needed" else {}
|
|
192
|
+
fields, notes = {}, []
|
|
193
|
+
base = ep["base_url"]
|
|
194
|
+
try:
|
|
195
|
+
r = get(base + "/models", headers=h, timeout=timeout)
|
|
196
|
+
data = r.json() if getattr(r, "status_code", 0) == 200 else None
|
|
197
|
+
rows = (data or {}).get("data") if isinstance(data, dict) else data
|
|
198
|
+
hit = next((e for e in rows or [] if isinstance(e, dict) and str(e.get("id")) in (ep["model"], bare(ep["model"]))), None)
|
|
199
|
+
if hit:
|
|
200
|
+
fields.update(from_models_entry(hit))
|
|
201
|
+
notes.append("/models")
|
|
202
|
+
elif rows is not None:
|
|
203
|
+
notes.append(f"/models does not list {ep['model']}")
|
|
204
|
+
except Exception as e: # unreachable, not JSON, ...: the table decides
|
|
205
|
+
notes.append(f"/models: {type(e).__name__}")
|
|
206
|
+
root = re.sub(r"/v1/?$", "", base)
|
|
207
|
+
if ep.get("preset") == "ollama" or ":11434" in base:
|
|
208
|
+
try:
|
|
209
|
+
r = post(root + "/api/show", json={"model": ep["model"]}, headers=h, timeout=timeout)
|
|
210
|
+
if getattr(r, "status_code", 0) == 200:
|
|
211
|
+
fields.update(from_ollama_show(r.json()))
|
|
212
|
+
notes.append("/api/show")
|
|
213
|
+
except Exception as e:
|
|
214
|
+
notes.append(f"/api/show: {type(e).__name__}")
|
|
215
|
+
if ep.get("preset") == "lmstudio" or ":1234" in base:
|
|
216
|
+
try:
|
|
217
|
+
r = get(root + "/api/v0/models/" + ep["model"], headers=h, timeout=timeout)
|
|
218
|
+
if getattr(r, "status_code", 0) == 200:
|
|
219
|
+
fields.update(from_models_entry(r.json()))
|
|
220
|
+
notes.append("/api/v0/models")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
notes.append(f"/api/v0/models: {type(e).__name__}")
|
|
223
|
+
_DETECTED[ck] = (fields, "; ".join(notes))
|
|
224
|
+
return _DETECTED[ck]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def resolve(ep, rules=None, key=None, get=None, post=None):
|
|
228
|
+
"""Profile for an endpoint dict (ecadlib.llm.endpoint), with detection for openai-compatible endpoints when
|
|
229
|
+
[ai] detect is on. Anthropic endpoints are never probed (their table entry is enough; nothing extra is sent)."""
|
|
230
|
+
det, note = (None, "")
|
|
231
|
+
if ep["provider"] == "openai-compatible" and ep.get("detect", True):
|
|
232
|
+
det, note = detect(ep, key, get, post)
|
|
233
|
+
p = profile(ep["model"], ep["provider"], rules, det)
|
|
234
|
+
if note:
|
|
235
|
+
p["detect"] = note
|
|
236
|
+
return p
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def describe(p):
|
|
240
|
+
caps = [c for c in ("pdf", "vision", "json_schema") if p.get(c)]
|
|
241
|
+
return f"{p['tier']} tier; {', '.join(caps) or 'text only'}; context {p['context']:,} tokens ({p['source']})"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def dumps(p):
|
|
245
|
+
return json.dumps(p, sort_keys=True)
|