parseforge 0.2.8__tar.gz → 0.2.9__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 (56) hide show
  1. {parseforge-0.2.8/parseforge.egg-info → parseforge-0.2.9}/PKG-INFO +32 -17
  2. {parseforge-0.2.8 → parseforge-0.2.9}/README.md +27 -16
  3. parseforge-0.2.9/parseforge/__init__.py +1 -0
  4. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/cli/config.py +18 -0
  5. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/cli/main.py +218 -18
  6. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/__init__.py +2 -0
  7. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/__init__.py +2 -0
  8. parseforge-0.2.9/parseforge/naming/providers/azure.py +197 -0
  9. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/pipeline.py +17 -0
  10. {parseforge-0.2.8 → parseforge-0.2.9/parseforge.egg-info}/PKG-INFO +32 -17
  11. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge.egg-info/SOURCES.txt +1 -0
  12. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge.egg-info/requires.txt +5 -0
  13. {parseforge-0.2.8 → parseforge-0.2.9}/pyproject.toml +7 -4
  14. parseforge-0.2.8/parseforge/__init__.py +0 -1
  15. {parseforge-0.2.8 → parseforge-0.2.9}/LICENSE +0 -0
  16. {parseforge-0.2.8 → parseforge-0.2.9}/MANIFEST.in +0 -0
  17. {parseforge-0.2.8 → parseforge-0.2.9}/SPEC.md +0 -0
  18. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/cli/__init__.py +0 -0
  19. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/drift.py +0 -0
  20. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/generation.py +0 -0
  21. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/integration.py +0 -0
  22. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/assemble.py +0 -0
  23. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/cache.py +0 -0
  24. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/llm.py +0 -0
  25. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/prompts.py +0 -0
  26. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/prompts.yaml +0 -0
  27. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/anthropic.py +0 -0
  28. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/cerebras.py +0 -0
  29. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/cohere.py +0 -0
  30. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/cost.py +0 -0
  31. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/deepseek.py +0 -0
  32. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/errors.py +0 -0
  33. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/fireworks.py +0 -0
  34. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/groq.py +0 -0
  35. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/mistral.py +0 -0
  36. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/models.py +0 -0
  37. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/models.yaml +0 -0
  38. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/moonshot.py +0 -0
  39. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/openai.py +0 -0
  40. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/openrouter.py +0 -0
  41. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/perplexity.py +0 -0
  42. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/text.py +0 -0
  43. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/together.py +0 -0
  44. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/providers/xai.py +0 -0
  45. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/naming/resolver.py +0 -0
  46. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/paths.py +0 -0
  47. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/promotion.py +0 -0
  48. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/sampling/__init__.py +0 -0
  49. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/sampling/backends/__init__.py +0 -0
  50. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/sampling/backends/netmiko.py +0 -0
  51. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/sampling/core.py +0 -0
  52. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge/validation.py +0 -0
  53. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge.egg-info/dependency_links.txt +0 -0
  54. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge.egg-info/entry_points.txt +0 -0
  55. {parseforge-0.2.8 → parseforge-0.2.9}/parseforge.egg-info/top_level.txt +0 -0
  56. {parseforge-0.2.8 → parseforge-0.2.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parseforge
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: LLM-driven pipeline that forges, validates, and promotes TextFSM templates from network CLI output
5
5
  Author-email: Tuyen Mathew Duong <tuyen@geekstrident.com>
6
6
  Maintainer-email: Tuyen Mathew Duong <tuyen@geekstrident.com>
@@ -66,6 +66,9 @@ Requires-Dist: textfsm-ai[mistral]>=0.6.1; extra == "mistral"
66
66
  Provides-Extra: cohere
67
67
  Requires-Dist: cohere==5.21.1; extra == "cohere"
68
68
  Requires-Dist: textfsm-ai[cohere]>=0.6.1; extra == "cohere"
69
+ Provides-Extra: azure
70
+ Requires-Dist: azure-ai-inference>=1.0.0b9; extra == "azure"
71
+ Requires-Dist: textfsm-ai[azure]>=0.6.1; extra == "azure"
69
72
  Provides-Extra: sampling
70
73
  Requires-Dist: netmiko>=4.0; extra == "sampling"
71
74
  Provides-Extra: dev
@@ -78,6 +81,7 @@ Requires-Dist: anthropic>=0.40.0; extra == "dev"
78
81
  Requires-Dist: openai>=1.0.0; extra == "dev"
79
82
  Requires-Dist: mistralai==1.10.0; extra == "dev"
80
83
  Requires-Dist: cohere==5.21.1; extra == "dev"
84
+ Requires-Dist: azure-ai-inference>=1.0.0b9; extra == "dev"
81
85
  Provides-Extra: release
82
86
  Requires-Dist: bump2version; extra == "release"
83
87
  Requires-Dist: build; extra == "release"
@@ -120,11 +124,11 @@ command that's pure local processing (`canonical`/`readable`/`recognizers`,
120
124
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
121
125
  extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
122
126
  `groq`, `xai`, `together`, `fireworks`, `perplexity`, `openrouter`,
123
- `moonshot`, `cerebras`, `mistral`, or `cohere`. `--provider` defaults to
124
- `anthropic` wherever it isn't required, so that's the one most setups need.
125
- `pip install parseforge[sampling]` adds Netmiko for live device sampling;
126
- combine extras as needed, e.g.
127
- `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,sampling]`.
127
+ `moonshot`, `cerebras`, `mistral`, `cohere`, or `azure`. `--provider` defaults
128
+ to `anthropic` wherever it isn't required, so that's the one most setups
129
+ need. `pip install parseforge[sampling]` adds Netmiko for live device
130
+ sampling; combine extras as needed, e.g.
131
+ `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,azure,sampling]`.
128
132
 
129
133
  ## Development
130
134
 
@@ -132,13 +136,13 @@ combine extras as needed, e.g.
132
136
  pip install -e ".[dev,sampling]"
133
137
  pytest
134
138
  ```
135
- `dev` already includes the `anthropic`, `openai`, `mistralai`, and `cohere`
136
- SDKs (tests exercise all thirteen providers — `anthropic`, `openai`,
137
- `deepseek`, `groq`, `xai`, `together`, `fireworks`, `perplexity`,
138
- `openrouter`, `moonshot`, `cerebras` share just the first two packages, and
139
- `mistral`/`cohere` each need their own native SDK — and never silently
140
- skip) — add the specific `,<provider>` extra explicitly only if installing
141
- outside of `dev`.
139
+ `dev` already includes the `anthropic`, `openai`, `mistralai`, `cohere`, and
140
+ `azure-ai-inference` SDKs (tests exercise all fourteen providers —
141
+ `anthropic`, `openai`, `deepseek`, `groq`, `xai`, `together`, `fireworks`,
142
+ `perplexity`, `openrouter`, `moonshot`, `cerebras` share just the first two
143
+ packages, and `mistral`/`cohere`/`azure` each need their own SDK — and never
144
+ silently skip) — add the specific `,<provider>` extra explicitly only if
145
+ installing outside of `dev`.
142
146
 
143
147
  Linting/formatting/type-checking/docs run through tox instead of extras — see
144
148
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -160,8 +164,14 @@ parseforge name --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.
160
164
  ```
161
165
  Still needs an LLM provider on a cache miss — `--provider` defaults to `anthropic`,
162
166
  and `--api-key` falls back to that provider's own env var (`ANTHROPIC_API_KEY`/
163
- `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
164
- never touches the LLM, so no key is needed at all in that case.
167
+ `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`/etc.). A cache hit (a command already seen
168
+ before) never touches the LLM, so no key is needed at all in that case.
169
+
170
+ `--provider azure` is the one exception to the usual `--api-key`/`--model` shape —
171
+ Azure has no fixed base_url or model catalog, so it also needs `--endpoint`
172
+ (`AZURE_ENDPOINT`), `--api-version` (`AZURE_API_VERSION`), and `--deployment`
173
+ (`AZURE_DEPLOYMENT`, replacing `--model`). Same three options exist on
174
+ `check --provider`, `run`, and `generate-template` below.
165
175
 
166
176
  **`check`** — validate a connector or provider before spending time/tokens on a real
167
177
  run. With neither `--env` nor explicit connection flags, prints what a connector needs
@@ -206,7 +216,10 @@ parseforge run --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.1
206
216
  `--naming-provider`/`--naming-api-key`/`--naming-model`, defaulting independently
207
217
  (`--naming-provider` defaults to `anthropic`; the other two fall back to that
208
218
  provider's own env var/default model) — set them explicitly if naming needs a
209
- different provider than generation.
219
+ different provider than generation. `--provider azure`/`--naming-provider azure`
220
+ each get their own `--endpoint`/`--api-version`/`--deployment` (generation) and
221
+ `--naming-endpoint`/`--naming-api-version`/`--naming-deployment` (naming) — see
222
+ the `name` section above for what they're for.
210
223
 
211
224
  **`init-trial-config`** — write a placeholder `trial.yaml` to fill in, instead of
212
225
  writing one by hand:
@@ -239,7 +252,9 @@ workers: 1
239
252
  ```
240
253
  `provider`/`api_key`/`model` are one shared LLM source used for both naming and
241
254
  generation. Use the `run` command's separate `--naming-*`/`--generation-*` flags
242
- instead if a trial actually needs two different providers.
255
+ instead if a trial actually needs two different providers. For `provider: azure`,
256
+ also set `endpoint`/`api_version`/`deployment` (`deployment` replaces `model`) —
257
+ see `parseforge init-trial-config`'s generated placeholder for the exact keys.
243
258
 
244
259
  **`integration`** — rebuild `integration/` for every case under `trials/` (SPEC §5
245
260
  step 8):
@@ -35,11 +35,11 @@ command that's pure local processing (`canonical`/`readable`/`recognizers`,
35
35
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
36
36
  extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
37
37
  `groq`, `xai`, `together`, `fireworks`, `perplexity`, `openrouter`,
38
- `moonshot`, `cerebras`, `mistral`, or `cohere`. `--provider` defaults to
39
- `anthropic` wherever it isn't required, so that's the one most setups need.
40
- `pip install parseforge[sampling]` adds Netmiko for live device sampling;
41
- combine extras as needed, e.g.
42
- `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,sampling]`.
38
+ `moonshot`, `cerebras`, `mistral`, `cohere`, or `azure`. `--provider` defaults
39
+ to `anthropic` wherever it isn't required, so that's the one most setups
40
+ need. `pip install parseforge[sampling]` adds Netmiko for live device
41
+ sampling; combine extras as needed, e.g.
42
+ `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,azure,sampling]`.
43
43
 
44
44
  ## Development
45
45
 
@@ -47,13 +47,13 @@ combine extras as needed, e.g.
47
47
  pip install -e ".[dev,sampling]"
48
48
  pytest
49
49
  ```
50
- `dev` already includes the `anthropic`, `openai`, `mistralai`, and `cohere`
51
- SDKs (tests exercise all thirteen providers — `anthropic`, `openai`,
52
- `deepseek`, `groq`, `xai`, `together`, `fireworks`, `perplexity`,
53
- `openrouter`, `moonshot`, `cerebras` share just the first two packages, and
54
- `mistral`/`cohere` each need their own native SDK — and never silently
55
- skip) — add the specific `,<provider>` extra explicitly only if installing
56
- outside of `dev`.
50
+ `dev` already includes the `anthropic`, `openai`, `mistralai`, `cohere`, and
51
+ `azure-ai-inference` SDKs (tests exercise all fourteen providers —
52
+ `anthropic`, `openai`, `deepseek`, `groq`, `xai`, `together`, `fireworks`,
53
+ `perplexity`, `openrouter`, `moonshot`, `cerebras` share just the first two
54
+ packages, and `mistral`/`cohere`/`azure` each need their own SDK — and never
55
+ silently skip) — add the specific `,<provider>` extra explicitly only if
56
+ installing outside of `dev`.
57
57
 
58
58
  Linting/formatting/type-checking/docs run through tox instead of extras — see
59
59
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -75,8 +75,14 @@ parseforge name --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.
75
75
  ```
76
76
  Still needs an LLM provider on a cache miss — `--provider` defaults to `anthropic`,
77
77
  and `--api-key` falls back to that provider's own env var (`ANTHROPIC_API_KEY`/
78
- `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
79
- never touches the LLM, so no key is needed at all in that case.
78
+ `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`/etc.). A cache hit (a command already seen
79
+ before) never touches the LLM, so no key is needed at all in that case.
80
+
81
+ `--provider azure` is the one exception to the usual `--api-key`/`--model` shape —
82
+ Azure has no fixed base_url or model catalog, so it also needs `--endpoint`
83
+ (`AZURE_ENDPOINT`), `--api-version` (`AZURE_API_VERSION`), and `--deployment`
84
+ (`AZURE_DEPLOYMENT`, replacing `--model`). Same three options exist on
85
+ `check --provider`, `run`, and `generate-template` below.
80
86
 
81
87
  **`check`** — validate a connector or provider before spending time/tokens on a real
82
88
  run. With neither `--env` nor explicit connection flags, prints what a connector needs
@@ -121,7 +127,10 @@ parseforge run --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.1
121
127
  `--naming-provider`/`--naming-api-key`/`--naming-model`, defaulting independently
122
128
  (`--naming-provider` defaults to `anthropic`; the other two fall back to that
123
129
  provider's own env var/default model) — set them explicitly if naming needs a
124
- different provider than generation.
130
+ different provider than generation. `--provider azure`/`--naming-provider azure`
131
+ each get their own `--endpoint`/`--api-version`/`--deployment` (generation) and
132
+ `--naming-endpoint`/`--naming-api-version`/`--naming-deployment` (naming) — see
133
+ the `name` section above for what they're for.
125
134
 
126
135
  **`init-trial-config`** — write a placeholder `trial.yaml` to fill in, instead of
127
136
  writing one by hand:
@@ -154,7 +163,9 @@ workers: 1
154
163
  ```
155
164
  `provider`/`api_key`/`model` are one shared LLM source used for both naming and
156
165
  generation. Use the `run` command's separate `--naming-*`/`--generation-*` flags
157
- instead if a trial actually needs two different providers.
166
+ instead if a trial actually needs two different providers. For `provider: azure`,
167
+ also set `endpoint`/`api_version`/`deployment` (`deployment` replaces `model`) —
168
+ see `parseforge init-trial-config`'s generated placeholder for the exact keys.
158
169
 
159
170
  **`integration`** — rebuild `integration/` for every case under `trials/` (SPEC §5
160
171
  step 8):
@@ -0,0 +1 @@
1
+ __version__ = "0.2.9"
@@ -42,6 +42,14 @@ class TrialConfig:
42
42
  note: str | None = None
43
43
  workers: int = 1
44
44
  path: str | None = None
45
+ # Only meaningful for provider: azure — no fixed base_url or model
46
+ # catalog there (see naming/providers/azure.py). endpoint/api_version
47
+ # are ignored by every other provider; deployment replaces the usual
48
+ # "model" concept and is what actually gets sent as the generation
49
+ # model argument (see cli/main.py's trial_cmd).
50
+ endpoint: str | None = None
51
+ api_version: str | None = None
52
+ deployment: str | None = None
45
53
 
46
54
 
47
55
  @dataclass(frozen=True)
@@ -57,6 +65,10 @@ class GenerationRequestConfig:
57
65
  env: str | None = None
58
66
  cmdline: str | None = None
59
67
  sample_file: str | None = None
68
+ # Only meaningful for provider: azure — see TrialConfig above.
69
+ endpoint: str | None = None
70
+ api_version: str | None = None
71
+ deployment: str | None = None
60
72
 
61
73
 
62
74
  _TRIAL_REQUIRED = (
@@ -124,6 +136,9 @@ def load_trial_config(path: Path) -> TrialConfig:
124
136
  note=raw.get("notes"),
125
137
  workers=int(raw.get("workers", 1)),
126
138
  path=raw.get("path"),
139
+ endpoint=raw.get("endpoint"),
140
+ api_version=raw.get("api_version"),
141
+ deployment=raw.get("deployment"),
127
142
  )
128
143
 
129
144
 
@@ -143,4 +158,7 @@ def load_generation_config(path: Path) -> GenerationRequestConfig:
143
158
  env=raw.get("env"),
144
159
  cmdline=raw.get("cmdline"),
145
160
  sample_file=raw.get("sample_file"),
161
+ endpoint=raw.get("endpoint"),
162
+ api_version=raw.get("api_version"),
163
+ deployment=raw.get("deployment"),
146
164
  )
@@ -20,9 +20,11 @@ from parseforge import (
20
20
  validation,
21
21
  )
22
22
  from parseforge.cli import config as cli_config
23
+ from parseforge.naming.providers.azure import DEFAULT_API_VERSION
23
24
 
24
25
  _BUILDERS: dict[str, type[naming.RegexBuilder]] = {
25
26
  "anthropic": naming.AnthropicRegexBuilder,
27
+ "azure": naming.AzureRegexBuilder,
26
28
  "cerebras": naming.CerebrasRegexBuilder,
27
29
  "cohere": naming.CohereRegexBuilder,
28
30
  "deepseek": naming.DeepSeekRegexBuilder,
@@ -61,6 +63,13 @@ device_type: <netmiko-device-type>
61
63
  provider: anthropic
62
64
  api_key: <api-key>
63
65
  model: <model>
66
+ # For provider: azure only -- no fixed base_url or model catalog there.
67
+ # deployment replaces model above (still keep a model: placeholder value,
68
+ # it's simply ignored); endpoint/api_version default to AZURE_ENDPOINT/
69
+ # AZURE_API_VERSION if omitted.
70
+ # endpoint: <azure-resource-endpoint>
71
+ # api_version: <azure-api-version>
72
+ # deployment: <azure-deployment-name>
64
73
 
65
74
  # --- commands to run (required, at least one) ---
66
75
  commands:
@@ -86,6 +95,10 @@ _GENERATE_TEMPLATE_CONFIG_PLACEHOLDER = """\
86
95
  provider: anthropic
87
96
  api_key: <api-key>
88
97
  model: <model>
98
+ # For provider: azure only -- see trial.yaml's placeholder for details.
99
+ # endpoint: <azure-resource-endpoint>
100
+ # api_version: <azure-api-version>
101
+ # deployment: <azure-deployment-name>
89
102
 
90
103
  # --- input: choose exactly one ---
91
104
 
@@ -114,13 +127,34 @@ def _write_placeholder_config(out_path: str, force: bool, content: str) -> None:
114
127
 
115
128
 
116
129
  def _build_regex_builder(
117
- provider: str, api_key: str | None, model: str | None
130
+ provider: str,
131
+ api_key: str | None,
132
+ model: str | None,
133
+ endpoint: str | None = None,
134
+ api_version: str | None = None,
135
+ deployment: str | None = None,
118
136
  ) -> naming.RegexBuilder:
137
+ """endpoint/api_version/deployment are only meaningful for
138
+ provider="azure" (no fixed base_url or model catalog there — see
139
+ naming/providers/azure.py) — forwarded only when given, so every
140
+ other provider's constructor never sees an unexpected kwarg.
141
+
142
+ When deployment is given, model is *not* forwarded — AzureRegexBuilder
143
+ has no model parameter at all (deployment replaces it), so passing
144
+ both would raise a plain TypeError. This matters in practice: every
145
+ TrialConfig always has a (placeholder) model value, since it's a
146
+ required config key shared by every provider."""
119
147
  kwargs: dict[str, str] = {}
120
148
  if api_key is not None:
121
149
  kwargs["api_key"] = api_key
122
- if model is not None:
150
+ if deployment is not None:
151
+ kwargs["deployment"] = deployment
152
+ elif model is not None:
123
153
  kwargs["model"] = model
154
+ if endpoint is not None:
155
+ kwargs["endpoint"] = endpoint
156
+ if api_version is not None:
157
+ kwargs["api_version"] = api_version
124
158
  return _BUILDERS[provider](**kwargs)
125
159
 
126
160
 
@@ -212,12 +246,31 @@ def main() -> None:
212
246
  "variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GROQ_API_KEY, "
213
247
  "XAI_API_KEY, TOGETHER_API_KEY, FIREWORKS_API_KEY, PERPLEXITY_API_KEY, "
214
248
  "OPENROUTER_API_KEY, MOONSHOT_API_KEY, CEREBRAS_API_KEY, MISTRAL_API_KEY, "
215
- "COHERE_API_KEY); only needed on a cache miss.",
249
+ "COHERE_API_KEY, AZURE_API_KEY); only needed on a cache miss.",
216
250
  )
217
251
  @click.option(
218
252
  "--model",
219
253
  default=None,
220
- help="Defaults to the selected provider's own default model.",
254
+ help="Defaults to the selected provider's own default model. Ignored for "
255
+ "--provider azure — use --deployment instead.",
256
+ )
257
+ @click.option(
258
+ "--endpoint",
259
+ default=None,
260
+ help="Azure resource endpoint. Only used by --provider azure; defaults to "
261
+ "AZURE_ENDPOINT.",
262
+ )
263
+ @click.option(
264
+ "--api-version",
265
+ default=None,
266
+ help="Azure API version. Only used by --provider azure; defaults to "
267
+ "AZURE_API_VERSION, then a built-in default.",
268
+ )
269
+ @click.option(
270
+ "--deployment",
271
+ default=None,
272
+ help="Azure deployment name — replaces --model for --provider azure; "
273
+ "defaults to AZURE_DEPLOYMENT.",
221
274
  )
222
275
  @click.argument("command", nargs=-1, required=True)
223
276
  def name_cmd(
@@ -228,6 +281,9 @@ def name_cmd(
228
281
  provider: str,
229
282
  api_key: str | None,
230
283
  model: str | None,
284
+ endpoint: str | None,
285
+ api_version: str | None,
286
+ deployment: str | None,
231
287
  command: tuple[str, ...],
232
288
  ) -> None:
233
289
  """Print the canonical cli-name for a raw CLI COMMAND.
@@ -236,7 +292,9 @@ def name_cmd(
236
292
  on a cache miss.
237
293
  """
238
294
  context = naming.CliContext(vendor=vendor, family=family, os=os_, version=version)
239
- builder = _build_regex_builder(provider, api_key, model)
295
+ builder = _build_regex_builder(
296
+ provider, api_key, model, endpoint, api_version, deployment
297
+ )
240
298
  click.echo(naming.cli_name(" ".join(command), context, builder=builder))
241
299
 
242
300
 
@@ -274,7 +332,26 @@ def name_cmd(
274
332
  @click.option(
275
333
  "--naming-model",
276
334
  default=None,
277
- help="Defaults to the naming provider's own default model.",
335
+ help="Defaults to the naming provider's own default model. Ignored for "
336
+ "--naming-provider azure — use --naming-deployment instead.",
337
+ )
338
+ @click.option(
339
+ "--naming-endpoint",
340
+ default=None,
341
+ help="Azure resource endpoint for naming. Only used by --naming-provider "
342
+ "azure; defaults to AZURE_ENDPOINT.",
343
+ )
344
+ @click.option(
345
+ "--naming-api-version",
346
+ default=None,
347
+ help="Azure API version for naming. Only used by --naming-provider azure; "
348
+ "defaults to AZURE_API_VERSION, then a built-in default.",
349
+ )
350
+ @click.option(
351
+ "--naming-deployment",
352
+ default=None,
353
+ help="Azure deployment name for naming — replaces --naming-model for "
354
+ "--naming-provider azure; defaults to AZURE_DEPLOYMENT.",
278
355
  )
279
356
  @click.option(
280
357
  "--provider",
@@ -282,11 +359,39 @@ def name_cmd(
282
359
  help="LLM provider for template generation (textfsm-ai's own registry, "
283
360
  'e.g. "anthropic", "openai", "deepseek", "groq", "xai", "together", '
284
361
  '"fireworks", "perplexity", "openrouter", "moonshot", "cerebras", '
285
- '"mistral", "cohere"). Naming uses its own separate --naming-provider, '
286
- "not this one.",
362
+ '"mistral", "cohere", "azure"). Naming uses its own separate '
363
+ "--naming-provider, not this one.",
287
364
  )
288
365
  @click.option("--api-key", required=True, help="API key for the generation LLM call.")
289
- @click.option("--model", required=True, help="Model for the generation LLM call.")
366
+ @click.option(
367
+ "--model",
368
+ required=True,
369
+ help="Model for the generation LLM call. For --provider azure, pass the "
370
+ "deployment name here via --deployment instead — this becomes the "
371
+ "deployment name that's actually sent.",
372
+ )
373
+ @click.option(
374
+ "--endpoint",
375
+ default=None,
376
+ envvar="AZURE_ENDPOINT",
377
+ help="Azure resource endpoint for generation. Only used by --provider "
378
+ "azure; defaults to the AZURE_ENDPOINT environment variable.",
379
+ )
380
+ @click.option(
381
+ "--api-version",
382
+ default=None,
383
+ envvar="AZURE_API_VERSION",
384
+ help="Azure API version for generation. Only used by --provider azure; "
385
+ "defaults to the AZURE_API_VERSION environment variable.",
386
+ )
387
+ @click.option(
388
+ "--deployment",
389
+ default=None,
390
+ envvar="AZURE_DEPLOYMENT",
391
+ help="Azure deployment name for generation — overrides --model for "
392
+ "--provider azure; defaults to the AZURE_DEPLOYMENT environment "
393
+ "variable.",
394
+ )
290
395
  @click.option(
291
396
  "--store-root",
292
397
  default=None,
@@ -308,9 +413,15 @@ def run_cmd(
308
413
  naming_provider: str,
309
414
  naming_api_key: str | None,
310
415
  naming_model: str | None,
416
+ naming_endpoint: str | None,
417
+ naming_api_version: str | None,
418
+ naming_deployment: str | None,
311
419
  provider: str,
312
420
  api_key: str,
313
421
  model: str,
422
+ endpoint: str | None,
423
+ api_version: str | None,
424
+ deployment: str | None,
314
425
  store_root: str | None,
315
426
  project: str | None,
316
427
  email: str | None,
@@ -335,9 +446,20 @@ def run_cmd(
335
446
  connection = DeviceConnection(
336
447
  host=host, username=username, password=password, device_type=device_type
337
448
  )
338
- naming_builder = _build_regex_builder(naming_provider, naming_api_key, naming_model)
449
+ naming_builder = _build_regex_builder(
450
+ naming_provider,
451
+ naming_api_key,
452
+ naming_model,
453
+ naming_endpoint,
454
+ naming_api_version,
455
+ naming_deployment,
456
+ )
339
457
  generation_config = LLMProviderConfig(
340
- provider=provider, api_key=api_key, model=model
458
+ provider=provider,
459
+ api_key=api_key,
460
+ model=deployment or model,
461
+ endpoint=endpoint,
462
+ api_version=api_version,
341
463
  )
342
464
  metadata = TrialMetadata(
343
465
  project=project,
@@ -391,8 +513,17 @@ def _check_connector(
391
513
  click.echo("OK")
392
514
 
393
515
 
394
- def _check_provider(provider: str, api_key: str | None, model: str | None) -> None:
395
- builder = _build_regex_builder(provider, api_key, model)
516
+ def _check_provider(
517
+ provider: str,
518
+ api_key: str | None,
519
+ model: str | None,
520
+ endpoint: str | None = None,
521
+ api_version: str | None = None,
522
+ deployment: str | None = None,
523
+ ) -> None:
524
+ builder = _build_regex_builder(
525
+ provider, api_key, model, endpoint, api_version, deployment
526
+ )
396
527
  context = naming.CliContext(vendor="check", family="check", os="check", version="0")
397
528
  try:
398
529
  response = builder.build_pattern("show clock", context)
@@ -425,6 +556,21 @@ def _check_provider(provider: str, api_key: str | None, model: str | None) -> No
425
556
  )
426
557
  @click.option("--api-key", default=None, help="Used for a --provider check.")
427
558
  @click.option("--model", default=None, help="Used for a --provider check.")
559
+ @click.option(
560
+ "--endpoint",
561
+ default=None,
562
+ help="Used for a --provider azure check; defaults to AZURE_ENDPOINT.",
563
+ )
564
+ @click.option(
565
+ "--api-version",
566
+ default=None,
567
+ help="Used for a --provider azure check; defaults to AZURE_API_VERSION.",
568
+ )
569
+ @click.option(
570
+ "--deployment",
571
+ default=None,
572
+ help="Used for a --provider azure check; defaults to AZURE_DEPLOYMENT.",
573
+ )
428
574
  def check_cmd(
429
575
  connector: str | None,
430
576
  provider: str | None,
@@ -436,6 +582,9 @@ def check_cmd(
436
582
  probe_command: str,
437
583
  api_key: str | None,
438
584
  model: str | None,
585
+ endpoint: str | None,
586
+ api_version: str | None,
587
+ deployment: str | None,
439
588
  ) -> None:
440
589
  """Validate a --connector or --provider is reachable, or report what it
441
590
  needs. Exactly one of --connector/--provider is required."""
@@ -447,7 +596,7 @@ def check_cmd(
447
596
  )
448
597
  else:
449
598
  assert provider is not None
450
- _check_provider(provider, api_key, model)
599
+ _check_provider(provider, api_key, model, endpoint, api_version, deployment)
451
600
 
452
601
 
453
602
  @main.command("init-generate-template-config")
@@ -492,7 +641,34 @@ def init_generate_template_config_cmd(out_path: str, force: bool) -> None:
492
641
  help='Generation LLM provider (textfsm-ai\'s own registry), e.g. "anthropic".',
493
642
  )
494
643
  @click.option("--api-key", default=None)
495
- @click.option("--model", default=None)
644
+ @click.option(
645
+ "--model",
646
+ default=None,
647
+ help="Model for the generation LLM call. For --provider azure, pass the "
648
+ "deployment name via --deployment instead — this becomes the "
649
+ "deployment name that's actually sent.",
650
+ )
651
+ @click.option(
652
+ "--endpoint",
653
+ default=None,
654
+ envvar="AZURE_ENDPOINT",
655
+ help="Azure resource endpoint. Only used by --provider azure; defaults to "
656
+ "the AZURE_ENDPOINT environment variable.",
657
+ )
658
+ @click.option(
659
+ "--api-version",
660
+ default=None,
661
+ envvar="AZURE_API_VERSION",
662
+ help="Azure API version. Only used by --provider azure; defaults to the "
663
+ "AZURE_API_VERSION environment variable.",
664
+ )
665
+ @click.option(
666
+ "--deployment",
667
+ default=None,
668
+ envvar="AZURE_DEPLOYMENT",
669
+ help="Azure deployment name — overrides --model for --provider azure; "
670
+ "defaults to the AZURE_DEPLOYMENT environment variable.",
671
+ )
496
672
  @click.option(
497
673
  "--out",
498
674
  "out_dir",
@@ -513,6 +689,9 @@ def generate_template_cmd(
513
689
  provider: str | None,
514
690
  api_key: str | None,
515
691
  model: str | None,
692
+ endpoint: str | None,
693
+ api_version: str | None,
694
+ deployment: str | None,
516
695
  out_dir: str | None,
517
696
  ) -> None:
518
697
  """One-shot template generation — no trial persisted under trials/.
@@ -533,6 +712,9 @@ def generate_template_cmd(
533
712
  provider = provider or cfg.provider
534
713
  api_key = api_key or cfg.api_key
535
714
  model = model or cfg.model
715
+ endpoint = endpoint or cfg.endpoint
716
+ api_version = api_version or cfg.api_version
717
+ deployment = deployment or cfg.deployment
536
718
 
537
719
  if not provider or not model or not api_key:
538
720
  raise click.UsageError(
@@ -552,7 +734,14 @@ def generate_template_cmd(
552
734
  "or --host/--username/--password/--device-type), or --config"
553
735
  )
554
736
 
555
- result = generation.generate(sample_text, provider, api_key, model)
737
+ result = generation.generate(
738
+ sample_text,
739
+ provider,
740
+ api_key,
741
+ deployment or model,
742
+ endpoint=endpoint or "",
743
+ api_version=api_version or DEFAULT_API_VERSION,
744
+ )
556
745
  if not result.ready:
557
746
  raise click.ClickException(f"generation not ready: {result.reason}")
558
747
 
@@ -685,9 +874,20 @@ def trial_cmd(config_path: str, store_root_opt: str | None) -> None:
685
874
  password=cfg.password,
686
875
  device_type=cfg.device_type,
687
876
  )
688
- naming_builder = _build_regex_builder(cfg.provider, cfg.api_key, cfg.model)
877
+ naming_builder = _build_regex_builder(
878
+ cfg.provider,
879
+ cfg.api_key,
880
+ cfg.model,
881
+ cfg.endpoint,
882
+ cfg.api_version,
883
+ cfg.deployment,
884
+ )
689
885
  generation_config = pipeline.LLMProviderConfig(
690
- provider=cfg.provider, api_key=cfg.api_key, model=cfg.model
886
+ provider=cfg.provider,
887
+ api_key=cfg.api_key,
888
+ model=cfg.deployment or cfg.model,
889
+ endpoint=cfg.endpoint,
890
+ api_version=cfg.api_version,
691
891
  )
692
892
  metadata = pipeline.TrialMetadata(
693
893
  username=cfg.user, email=cfg.email, description=cfg.description, note=cfg.note
@@ -10,6 +10,7 @@ from .llm import (
10
10
  )
11
11
  from .providers import (
12
12
  AnthropicRegexBuilder,
13
+ AzureRegexBuilder,
13
14
  CerebrasRegexBuilder,
14
15
  CohereRegexBuilder,
15
16
  DeepSeekRegexBuilder,
@@ -33,6 +34,7 @@ __all__ = [
33
34
  "RegexBuilder",
34
35
  "UnimplementedRegexBuilder",
35
36
  "AnthropicRegexBuilder",
37
+ "AzureRegexBuilder",
36
38
  "CerebrasRegexBuilder",
37
39
  "CohereRegexBuilder",
38
40
  "DeepSeekRegexBuilder",
@@ -1,4 +1,5 @@
1
1
  from .anthropic import AnthropicRegexBuilder
2
+ from .azure import AzureRegexBuilder
2
3
  from .cerebras import CerebrasRegexBuilder
3
4
  from .cohere import CohereRegexBuilder
4
5
  from .deepseek import DeepSeekRegexBuilder
@@ -14,6 +15,7 @@ from .xai import XAIRegexBuilder
14
15
 
15
16
  __all__ = [
16
17
  "AnthropicRegexBuilder",
18
+ "AzureRegexBuilder",
17
19
  "CerebrasRegexBuilder",
18
20
  "CohereRegexBuilder",
19
21
  "DeepSeekRegexBuilder",
@@ -0,0 +1,197 @@
1
+ """Azure OpenAI-backed RegexBuilder implementation.
2
+
3
+ Unlike every other provider, Azure has no fixed base_url or model catalog:
4
+ the endpoint is the caller's own Azure resource, and the usual "model"
5
+ concept is replaced by an account-specific deployment name. Talks to the
6
+ ``azure-ai-inference`` SDK's ``ChatCompletionsClient`` directly — not
7
+ OpenAI-compatible, not Anthropic-style, not Mistral/Cohere-style either,
8
+ though its response shape (``choices[0].message.content``,
9
+ ``finish_reason``, ``usage.prompt_tokens/completion_tokens/total_tokens``)
10
+ happens to closely mirror OpenAI's.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import time
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
20
+ from .cost import estimate_cost
21
+ from .text import extract_pattern
22
+
23
+ if TYPE_CHECKING:
24
+ from azure.ai.inference import ChatCompletionsClient as _ChatCompletionsClient
25
+
26
+ _DEFAULT_MAX_TOKENS = 1024
27
+
28
+ # Matches textfsm-ai's own AzureOpenAIProvider.from_env() default.
29
+ DEFAULT_API_VERSION = "2024-02-15-preview"
30
+
31
+ # HTTP statuses where retrying the exact same request can't succeed —
32
+ # mirrors providers/errors.py's NON_RETRYABLE class-name set, but the
33
+ # azure-ai-inference SDK raises a single HttpResponseError for every HTTP
34
+ # failure (status on exc.status_code, populated from the underlying HTTP
35
+ # response) rather than a family of named exception classes, so
36
+ # classification here is done by status code — same approach as Mistral/
37
+ # Cohere.
38
+ _NON_RETRYABLE_STATUSES = frozenset({400, 401, 403, 404, 409, 413, 422})
39
+
40
+
41
+ def _import_azure() -> Any:
42
+ """Deferred import — azure-ai-inference is an optional extra
43
+ (parseforge[azure]); nothing else in this module (or a cache-hit
44
+ lookup, which never reaches build_pattern at all — see
45
+ resolver.cli_name) should require it to be installed."""
46
+ try:
47
+ from azure.ai.inference import ChatCompletionsClient
48
+ from azure.core.credentials import AzureKeyCredential
49
+ from azure.core.exceptions import HttpResponseError
50
+ except ImportError as exc:
51
+ raise ImportError(
52
+ "the azure-ai-inference package is required to use "
53
+ "AzureRegexBuilder — install it via `pip install parseforge[azure]`"
54
+ ) from exc
55
+ return ChatCompletionsClient, AzureKeyCredential, HttpResponseError
56
+
57
+
58
+ def _build_deployment_endpoint(endpoint: str, deployment: str) -> str:
59
+ """Normalize a base Azure resource endpoint into the full deployment
60
+ endpoint azure-ai-inference's ChatCompletionsClient expects — mirrors
61
+ textfsm-ai's own build_azure_endpoint(). If the caller already passed
62
+ a full deployment endpoint, it's returned unchanged."""
63
+ endpoint = endpoint.rstrip("/")
64
+ if "/openai/deployments/" in endpoint:
65
+ return endpoint
66
+ return f"{endpoint}/openai/deployments/{deployment}"
67
+
68
+
69
+ def _is_retryable(exc: Any) -> bool:
70
+ return getattr(exc, "status_code", None) not in _NON_RETRYABLE_STATUSES
71
+
72
+
73
+ def _format_error_reason(exc: Any) -> str:
74
+ return f"LLM-ERROR-azure_sdk-{getattr(exc, 'status_code', None)}-{exc}"
75
+
76
+
77
+ class AzureRegexBuilder:
78
+ """Builds a cli-name regex pattern by prompting an Azure OpenAI deployment.
79
+
80
+ Unlike every other provider, there's no fixed model catalog — the
81
+ ``deployment`` argument (an account-specific deployment name) is
82
+ required, resolving from the argument if given, otherwise the
83
+ ``AZURE_DEPLOYMENT`` environment variable. ``endpoint`` (the caller's
84
+ own Azure resource URL) similarly resolves from the argument, then
85
+ ``AZURE_ENDPOINT``. ``api_version`` resolves from the argument, then
86
+ ``AZURE_API_VERSION``, then a default matching textfsm-ai's own
87
+ AzureOpenAIProvider.
88
+
89
+ The client is constructed lazily, on the first actual call — not in
90
+ ``__init__`` — so this can be used as a default RegexBuilder without
91
+ requiring any of these to be set for cache-hit lookups, which never
92
+ reach the LLM at all (see resolver.cli_name).
93
+ """
94
+
95
+ provider = "azure"
96
+
97
+ def __init__(
98
+ self,
99
+ deployment: str | None = None,
100
+ api_key: str | None = None,
101
+ endpoint: str | None = None,
102
+ api_version: str | None = None,
103
+ ) -> None:
104
+ self._deployment = deployment
105
+ self._api_key = api_key
106
+ self._endpoint = endpoint
107
+ self._api_version = api_version
108
+ self._resolved_deployment: str | None = None
109
+ self._client: _ChatCompletionsClient | None = None
110
+
111
+ def _get_client(self) -> _ChatCompletionsClient:
112
+ if self._client is None:
113
+ ChatCompletionsClient, AzureKeyCredential, _ = _import_azure()
114
+ api_key = self._api_key or os.environ.get("AZURE_API_KEY")
115
+ if not api_key:
116
+ raise RuntimeError(
117
+ "no Azure API key — pass api_key or set AZURE_API_KEY"
118
+ )
119
+ endpoint = self._endpoint or os.environ.get("AZURE_ENDPOINT")
120
+ if not endpoint:
121
+ raise RuntimeError(
122
+ "no Azure endpoint — pass endpoint or set AZURE_ENDPOINT"
123
+ )
124
+ deployment = self._deployment or os.environ.get("AZURE_DEPLOYMENT")
125
+ if not deployment:
126
+ raise RuntimeError(
127
+ "no Azure deployment — pass deployment or set AZURE_DEPLOYMENT"
128
+ )
129
+ api_version = (
130
+ self._api_version
131
+ or os.environ.get("AZURE_API_VERSION")
132
+ or DEFAULT_API_VERSION
133
+ )
134
+ self._resolved_deployment = deployment
135
+ self._client = ChatCompletionsClient(
136
+ endpoint=_build_deployment_endpoint(endpoint, deployment),
137
+ credential=AzureKeyCredential(api_key),
138
+ api_version=api_version,
139
+ )
140
+ return self._client
141
+
142
+ def build_pattern(
143
+ self, command: str, context: CliContext, **kwargs: Any
144
+ ) -> LLMCLIResponse:
145
+ _, _, HttpResponseError = _import_azure()
146
+ prompt = build_prompt(command, context)
147
+ max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
148
+
149
+ start = time.monotonic()
150
+ try:
151
+ response = self._get_client().complete(
152
+ messages=[{"role": "user", "content": prompt}],
153
+ max_tokens=max_tokens,
154
+ **kwargs,
155
+ )
156
+ except HttpResponseError as exc:
157
+ if not _is_retryable(exc):
158
+ # Same request would fail the same way again — stop rather
159
+ # than let a caller burn another attempt on it.
160
+ raise
161
+ return LLMCLIResponse(
162
+ content="",
163
+ raw=exc,
164
+ usage=TokenUsage(
165
+ input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
166
+ ),
167
+ duration_ms=(time.monotonic() - start) * 1000,
168
+ reason=_format_error_reason(exc),
169
+ ready=False,
170
+ )
171
+ duration_ms = (time.monotonic() - start) * 1000
172
+
173
+ choice = response.choices[0]
174
+ return LLMCLIResponse(
175
+ content=extract_pattern(choice.message.content or ""),
176
+ raw=response,
177
+ usage=TokenUsage(
178
+ input_tokens=response.usage.prompt_tokens,
179
+ output_tokens=response.usage.completion_tokens,
180
+ total_tokens=response.usage.total_tokens,
181
+ estimated_cost=estimate_cost(
182
+ input_tokens=response.usage.prompt_tokens,
183
+ output_tokens=response.usage.completion_tokens,
184
+ total_tokens=response.usage.total_tokens,
185
+ provider=self.provider,
186
+ # No public pricing table for a customer's own Azure
187
+ # deployment — estimate_cost() falls back to 0.0 with
188
+ # a warning for an unrecognized (provider, model) pair,
189
+ # which is the correct/expected outcome here, not an
190
+ # error condition.
191
+ model=self._resolved_deployment or "",
192
+ ),
193
+ ),
194
+ duration_ms=duration_ms,
195
+ reason=choice.finish_reason or "",
196
+ ready=choice.finish_reason == "stop",
197
+ )
@@ -46,6 +46,7 @@ from pathlib import Path
46
46
  from typing import Any
47
47
 
48
48
  from parseforge import generation, naming, paths, sampling, validation
49
+ from parseforge.naming.providers.azure import DEFAULT_API_VERSION
49
50
 
50
51
 
51
52
  class Mode(str, Enum):
@@ -55,9 +56,18 @@ class Mode(str, Enum):
55
56
 
56
57
  @dataclass(frozen=True)
57
58
  class LLMProviderConfig:
59
+ """``model`` doubles as the deployment name for provider="azure" (no
60
+ fixed model catalog there — see naming/providers/azure.py); the CLI
61
+ resolves --deployment into this field so this dataclass and
62
+ generation.generate()'s positional model arg stay untouched.
63
+ ``endpoint``/``api_version`` are ignored by every provider except
64
+ azure (see textfsm-ai's generation_engine.run())."""
65
+
58
66
  provider: str
59
67
  api_key: str
60
68
  model: str
69
+ endpoint: str | None = None
70
+ api_version: str | None = None
61
71
 
62
72
 
63
73
  @dataclass(frozen=True)
@@ -153,6 +163,13 @@ def run_command_pipeline(
153
163
  generation_config.provider,
154
164
  generation_config.api_key,
155
165
  generation_config.model,
166
+ endpoint=generation_config.endpoint or "",
167
+ # Only meaningful for provider="azure" (ignored by every other
168
+ # provider — see textfsm-ai's generation_engine.run()), which has
169
+ # no from_env()-style fallback of its own on this path, unlike
170
+ # naming/providers/azure.py's own AzureRegexBuilder — so the same
171
+ # default is applied here explicitly.
172
+ api_version=generation_config.api_version or DEFAULT_API_VERSION,
156
173
  )
157
174
  (derive_dir / "llm-template.textfsm").write_text(
158
175
  gen_result.raw_template, encoding="utf-8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parseforge
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: LLM-driven pipeline that forges, validates, and promotes TextFSM templates from network CLI output
5
5
  Author-email: Tuyen Mathew Duong <tuyen@geekstrident.com>
6
6
  Maintainer-email: Tuyen Mathew Duong <tuyen@geekstrident.com>
@@ -66,6 +66,9 @@ Requires-Dist: textfsm-ai[mistral]>=0.6.1; extra == "mistral"
66
66
  Provides-Extra: cohere
67
67
  Requires-Dist: cohere==5.21.1; extra == "cohere"
68
68
  Requires-Dist: textfsm-ai[cohere]>=0.6.1; extra == "cohere"
69
+ Provides-Extra: azure
70
+ Requires-Dist: azure-ai-inference>=1.0.0b9; extra == "azure"
71
+ Requires-Dist: textfsm-ai[azure]>=0.6.1; extra == "azure"
69
72
  Provides-Extra: sampling
70
73
  Requires-Dist: netmiko>=4.0; extra == "sampling"
71
74
  Provides-Extra: dev
@@ -78,6 +81,7 @@ Requires-Dist: anthropic>=0.40.0; extra == "dev"
78
81
  Requires-Dist: openai>=1.0.0; extra == "dev"
79
82
  Requires-Dist: mistralai==1.10.0; extra == "dev"
80
83
  Requires-Dist: cohere==5.21.1; extra == "dev"
84
+ Requires-Dist: azure-ai-inference>=1.0.0b9; extra == "dev"
81
85
  Provides-Extra: release
82
86
  Requires-Dist: bump2version; extra == "release"
83
87
  Requires-Dist: build; extra == "release"
@@ -120,11 +124,11 @@ command that's pure local processing (`canonical`/`readable`/`recognizers`,
120
124
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
121
125
  extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
122
126
  `groq`, `xai`, `together`, `fireworks`, `perplexity`, `openrouter`,
123
- `moonshot`, `cerebras`, `mistral`, or `cohere`. `--provider` defaults to
124
- `anthropic` wherever it isn't required, so that's the one most setups need.
125
- `pip install parseforge[sampling]` adds Netmiko for live device sampling;
126
- combine extras as needed, e.g.
127
- `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,sampling]`.
127
+ `moonshot`, `cerebras`, `mistral`, `cohere`, or `azure`. `--provider` defaults
128
+ to `anthropic` wherever it isn't required, so that's the one most setups
129
+ need. `pip install parseforge[sampling]` adds Netmiko for live device
130
+ sampling; combine extras as needed, e.g.
131
+ `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,azure,sampling]`.
128
132
 
129
133
  ## Development
130
134
 
@@ -132,13 +136,13 @@ combine extras as needed, e.g.
132
136
  pip install -e ".[dev,sampling]"
133
137
  pytest
134
138
  ```
135
- `dev` already includes the `anthropic`, `openai`, `mistralai`, and `cohere`
136
- SDKs (tests exercise all thirteen providers — `anthropic`, `openai`,
137
- `deepseek`, `groq`, `xai`, `together`, `fireworks`, `perplexity`,
138
- `openrouter`, `moonshot`, `cerebras` share just the first two packages, and
139
- `mistral`/`cohere` each need their own native SDK — and never silently
140
- skip) — add the specific `,<provider>` extra explicitly only if installing
141
- outside of `dev`.
139
+ `dev` already includes the `anthropic`, `openai`, `mistralai`, `cohere`, and
140
+ `azure-ai-inference` SDKs (tests exercise all fourteen providers —
141
+ `anthropic`, `openai`, `deepseek`, `groq`, `xai`, `together`, `fireworks`,
142
+ `perplexity`, `openrouter`, `moonshot`, `cerebras` share just the first two
143
+ packages, and `mistral`/`cohere`/`azure` each need their own SDK — and never
144
+ silently skip) — add the specific `,<provider>` extra explicitly only if
145
+ installing outside of `dev`.
142
146
 
143
147
  Linting/formatting/type-checking/docs run through tox instead of extras — see
144
148
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -160,8 +164,14 @@ parseforge name --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.
160
164
  ```
161
165
  Still needs an LLM provider on a cache miss — `--provider` defaults to `anthropic`,
162
166
  and `--api-key` falls back to that provider's own env var (`ANTHROPIC_API_KEY`/
163
- `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
164
- never touches the LLM, so no key is needed at all in that case.
167
+ `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`/etc.). A cache hit (a command already seen
168
+ before) never touches the LLM, so no key is needed at all in that case.
169
+
170
+ `--provider azure` is the one exception to the usual `--api-key`/`--model` shape —
171
+ Azure has no fixed base_url or model catalog, so it also needs `--endpoint`
172
+ (`AZURE_ENDPOINT`), `--api-version` (`AZURE_API_VERSION`), and `--deployment`
173
+ (`AZURE_DEPLOYMENT`, replacing `--model`). Same three options exist on
174
+ `check --provider`, `run`, and `generate-template` below.
165
175
 
166
176
  **`check`** — validate a connector or provider before spending time/tokens on a real
167
177
  run. With neither `--env` nor explicit connection flags, prints what a connector needs
@@ -206,7 +216,10 @@ parseforge run --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.1
206
216
  `--naming-provider`/`--naming-api-key`/`--naming-model`, defaulting independently
207
217
  (`--naming-provider` defaults to `anthropic`; the other two fall back to that
208
218
  provider's own env var/default model) — set them explicitly if naming needs a
209
- different provider than generation.
219
+ different provider than generation. `--provider azure`/`--naming-provider azure`
220
+ each get their own `--endpoint`/`--api-version`/`--deployment` (generation) and
221
+ `--naming-endpoint`/`--naming-api-version`/`--naming-deployment` (naming) — see
222
+ the `name` section above for what they're for.
210
223
 
211
224
  **`init-trial-config`** — write a placeholder `trial.yaml` to fill in, instead of
212
225
  writing one by hand:
@@ -239,7 +252,9 @@ workers: 1
239
252
  ```
240
253
  `provider`/`api_key`/`model` are one shared LLM source used for both naming and
241
254
  generation. Use the `run` command's separate `--naming-*`/`--generation-*` flags
242
- instead if a trial actually needs two different providers.
255
+ instead if a trial actually needs two different providers. For `provider: azure`,
256
+ also set `endpoint`/`api_version`/`deployment` (`deployment` replaces `model`) —
257
+ see `parseforge init-trial-config`'s generated placeholder for the exact keys.
243
258
 
244
259
  **`integration`** — rebuild `integration/` for every case under `trials/` (SPEC §5
245
260
  step 8):
@@ -29,6 +29,7 @@ parseforge/naming/prompts.yaml
29
29
  parseforge/naming/resolver.py
30
30
  parseforge/naming/providers/__init__.py
31
31
  parseforge/naming/providers/anthropic.py
32
+ parseforge/naming/providers/azure.py
32
33
  parseforge/naming/providers/cerebras.py
33
34
  parseforge/naming/providers/cohere.py
34
35
  parseforge/naming/providers/cost.py
@@ -7,6 +7,10 @@ textfsm-ai>=0.6.1
7
7
  anthropic>=0.40.0
8
8
  textfsm-ai[anthropic]>=0.6.1
9
9
 
10
+ [azure]
11
+ azure-ai-inference>=1.0.0b9
12
+ textfsm-ai[azure]>=0.6.1
13
+
10
14
  [cerebras]
11
15
  openai>=1.0.0
12
16
  textfsm-ai[cerebras]>=0.6.1
@@ -29,6 +33,7 @@ anthropic>=0.40.0
29
33
  openai>=1.0.0
30
34
  mistralai==1.10.0
31
35
  cohere==5.21.1
36
+ azure-ai-inference>=1.0.0b9
32
37
 
33
38
  [fireworks]
34
39
  openai>=1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "parseforge"
7
- version = "0.2.8"
7
+ version = "0.2.9"
8
8
  description = "LLM-driven pipeline that forges, validates, and promotes TextFSM templates from network CLI output"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -81,6 +81,7 @@ mistral = ["mistralai==1.10.0", "textfsm-ai[mistral]>=0.6.1"]
81
81
  # supporting Python 3.9 (6.x/7.x require Python >=3.10), matching
82
82
  # textfsm-ai's own pin.
83
83
  cohere = ["cohere==5.21.1", "textfsm-ai[cohere]>=0.6.1"]
84
+ azure = ["azure-ai-inference>=1.0.0b9", "textfsm-ai[azure]>=0.6.1"]
84
85
  sampling = ["netmiko>=4.0"]
85
86
  dev = [
86
87
  "pytest>=8.0.0",
@@ -88,13 +89,15 @@ dev = [
88
89
  "ruff>=0.4.0",
89
90
  "black>=24.0.0",
90
91
  "mypy>=1.10.0",
91
- # anthropic/openai/mistralai/cohere are optional at runtime (see the
92
- # extras above), but every provider is exercised by the CLI/tests,
93
- # which should never silently skip -- so dev always pulls all four in.
92
+ # anthropic/openai/mistralai/cohere/azure-ai-inference are optional at
93
+ # runtime (see the extras above), but every provider is exercised by
94
+ # the CLI/tests, which should never silently skip -- so dev always
95
+ # pulls all five in.
94
96
  "anthropic>=0.40.0",
95
97
  "openai>=1.0.0",
96
98
  "mistralai==1.10.0",
97
99
  "cohere==5.21.1",
100
+ "azure-ai-inference>=1.0.0b9",
98
101
  ]
99
102
  # Tooling for scripts/release.ps1 and CI's own package-build step.
100
103
  # bump2version isn't otherwise installed anywhere -- release.ps1 assumes
@@ -1 +0,0 @@
1
- __version__ = "0.2.8"
File without changes
File without changes
File without changes
File without changes