parseforge 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 (56) hide show
  1. {parseforge-0.2.4/parseforge.egg-info → parseforge-0.2.6}/PKG-INFO +42 -11
  2. {parseforge-0.2.4 → parseforge-0.2.6}/README.md +14 -10
  3. parseforge-0.2.6/parseforge/__init__.py +1 -0
  4. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/cli/main.py +16 -3
  5. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/__init__.py +22 -1
  6. parseforge-0.2.6/parseforge/naming/providers/__init__.py +25 -0
  7. parseforge-0.2.6/parseforge/naming/providers/cerebras.py +128 -0
  8. parseforge-0.2.6/parseforge/naming/providers/fireworks.py +128 -0
  9. parseforge-0.2.6/parseforge/naming/providers/groq.py +126 -0
  10. parseforge-0.2.6/parseforge/naming/providers/models.yaml +112 -0
  11. parseforge-0.2.6/parseforge/naming/providers/moonshot.py +130 -0
  12. parseforge-0.2.6/parseforge/naming/providers/openai.py +117 -0
  13. parseforge-0.2.6/parseforge/naming/providers/openrouter.py +130 -0
  14. parseforge-0.2.6/parseforge/naming/providers/perplexity.py +128 -0
  15. parseforge-0.2.6/parseforge/naming/providers/together.py +128 -0
  16. parseforge-0.2.6/parseforge/naming/providers/xai.py +126 -0
  17. {parseforge-0.2.4 → parseforge-0.2.6/parseforge.egg-info}/PKG-INFO +42 -11
  18. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge.egg-info/SOURCES.txt +9 -0
  19. parseforge-0.2.6/parseforge.egg-info/requires.txt +64 -0
  20. {parseforge-0.2.4 → parseforge-0.2.6}/pyproject.toml +10 -1
  21. parseforge-0.2.4/parseforge/__init__.py +0 -1
  22. parseforge-0.2.4/parseforge/naming/providers/__init__.py +0 -4
  23. parseforge-0.2.4/parseforge/naming/providers/models.yaml +0 -24
  24. parseforge-0.2.4/parseforge.egg-info/requires.txt +0 -28
  25. {parseforge-0.2.4 → parseforge-0.2.6}/LICENSE +0 -0
  26. {parseforge-0.2.4 → parseforge-0.2.6}/MANIFEST.in +0 -0
  27. {parseforge-0.2.4 → parseforge-0.2.6}/SPEC.md +0 -0
  28. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/cli/__init__.py +0 -0
  29. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/cli/config.py +0 -0
  30. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/drift.py +0 -0
  31. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/generation.py +0 -0
  32. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/integration.py +0 -0
  33. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/assemble.py +0 -0
  34. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/cache.py +0 -0
  35. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/llm.py +0 -0
  36. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/prompts.py +0 -0
  37. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/prompts.yaml +0 -0
  38. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/providers/anthropic.py +0 -0
  39. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/providers/cost.py +0 -0
  40. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/providers/deepseek.py +0 -0
  41. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/providers/errors.py +0 -0
  42. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/providers/models.py +0 -0
  43. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/providers/text.py +0 -0
  44. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/naming/resolver.py +0 -0
  45. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/paths.py +0 -0
  46. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/pipeline.py +0 -0
  47. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/promotion.py +0 -0
  48. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/sampling/__init__.py +0 -0
  49. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/sampling/backends/__init__.py +0 -0
  50. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/sampling/backends/netmiko.py +0 -0
  51. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/sampling/core.py +0 -0
  52. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge/validation.py +0 -0
  53. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge.egg-info/dependency_links.txt +0 -0
  54. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge.egg-info/entry_points.txt +0 -0
  55. {parseforge-0.2.4 → parseforge-0.2.6}/parseforge.egg-info/top_level.txt +0 -0
  56. {parseforge-0.2.4 → parseforge-0.2.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parseforge
3
- Version: 0.2.4
3
+ Version: 0.2.6
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>
@@ -33,6 +33,33 @@ Requires-Dist: textfsm-ai[anthropic]>=0.6.1; extra == "anthropic"
33
33
  Provides-Extra: deepseek
34
34
  Requires-Dist: openai>=1.0.0; extra == "deepseek"
35
35
  Requires-Dist: textfsm-ai[deepseek]>=0.6.1; extra == "deepseek"
36
+ Provides-Extra: openai
37
+ Requires-Dist: openai>=1.0.0; extra == "openai"
38
+ Requires-Dist: textfsm-ai[openai]>=0.6.1; extra == "openai"
39
+ Provides-Extra: groq
40
+ Requires-Dist: openai>=1.0.0; extra == "groq"
41
+ Requires-Dist: textfsm-ai[groq]>=0.6.1; extra == "groq"
42
+ Provides-Extra: xai
43
+ Requires-Dist: openai>=1.0.0; extra == "xai"
44
+ Requires-Dist: textfsm-ai[xai]>=0.6.1; extra == "xai"
45
+ Provides-Extra: together
46
+ Requires-Dist: openai>=1.0.0; extra == "together"
47
+ Requires-Dist: textfsm-ai[together]>=0.6.1; extra == "together"
48
+ Provides-Extra: fireworks
49
+ Requires-Dist: openai>=1.0.0; extra == "fireworks"
50
+ Requires-Dist: textfsm-ai[fireworks]>=0.6.1; extra == "fireworks"
51
+ Provides-Extra: perplexity
52
+ Requires-Dist: openai>=1.0.0; extra == "perplexity"
53
+ Requires-Dist: textfsm-ai[perplexity]>=0.6.1; extra == "perplexity"
54
+ Provides-Extra: openrouter
55
+ Requires-Dist: openai>=1.0.0; extra == "openrouter"
56
+ Requires-Dist: textfsm-ai[openrouter]>=0.6.1; extra == "openrouter"
57
+ Provides-Extra: moonshot
58
+ Requires-Dist: openai>=1.0.0; extra == "moonshot"
59
+ Requires-Dist: textfsm-ai[moonshot]>=0.6.1; extra == "moonshot"
60
+ Provides-Extra: cerebras
61
+ Requires-Dist: openai>=1.0.0; extra == "cerebras"
62
+ Requires-Dist: textfsm-ai[cerebras]>=0.6.1; extra == "cerebras"
36
63
  Provides-Extra: sampling
37
64
  Requires-Dist: netmiko>=4.0; extra == "sampling"
38
65
  Provides-Extra: dev
@@ -83,11 +110,13 @@ pip install parseforge[anthropic]
83
110
  command that's pure local processing (`canonical`/`readable`/`recognizers`,
84
111
  `integration`, `promotion`) works with nothing further. Anything that calls an
85
112
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
86
- extra for whichever provider it uses: `anthropic` or `deepseek`. `--provider`
87
- defaults to `anthropic` wherever it isn't required, so that's the one most
88
- setups need. `pip install parseforge[sampling]` adds Netmiko for live device
89
- sampling; combine extras as needed, e.g.
90
- `pip install parseforge[anthropic,deepseek,sampling]`.
113
+ extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
114
+ `groq`, `xai`, `together`, `fireworks`, `perplexity`, `openrouter`,
115
+ `moonshot`, or `cerebras`. `--provider` defaults to `anthropic` wherever it
116
+ isn't required, so that's the one most setups need. `pip install
117
+ parseforge[sampling]` adds Netmiko for live device sampling; combine extras
118
+ as needed, e.g.
119
+ `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,sampling]`.
91
120
 
92
121
  ## Development
93
122
 
@@ -95,9 +124,11 @@ sampling; combine extras as needed, e.g.
95
124
  pip install -e ".[dev,sampling]"
96
125
  pytest
97
126
  ```
98
- `dev` already includes both `anthropic` and `openai` (tests exercise both
99
- providers, never silently skip) add `,anthropic`/`,deepseek` explicitly only
100
- if installing outside of `dev`.
127
+ `dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
128
+ all eleven providers`anthropic`, `openai`, `deepseek`, `groq`, `xai`,
129
+ `together`, `fireworks`, `perplexity`, `openrouter`, `moonshot`, `cerebras`
130
+ share just those two packages — and never silently skip) — add the specific
131
+ `,<provider>` extra explicitly only if installing outside of `dev`.
101
132
 
102
133
  Linting/formatting/type-checking/docs run through tox instead of extras — see
103
134
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -119,8 +150,8 @@ parseforge name --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.
119
150
  ```
120
151
  Still needs an LLM provider on a cache miss — `--provider` defaults to `anthropic`,
121
152
  and `--api-key` falls back to that provider's own env var (`ANTHROPIC_API_KEY`/
122
- `DEEPSEEK_API_KEY`). A cache hit (a command already seen before) never touches the
123
- LLM, so no key is needed at all in that case.
153
+ `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
154
+ never touches the LLM, so no key is needed at all in that case.
124
155
 
125
156
  **`check`** — validate a connector or provider before spending time/tokens on a real
126
157
  run. With neither `--env` nor explicit connection flags, prints what a connector needs
@@ -33,11 +33,13 @@ pip install parseforge[anthropic]
33
33
  command that's pure local processing (`canonical`/`readable`/`recognizers`,
34
34
  `integration`, `promotion`) works with nothing further. Anything that calls an
35
35
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
36
- extra for whichever provider it uses: `anthropic` or `deepseek`. `--provider`
37
- defaults to `anthropic` wherever it isn't required, so that's the one most
38
- setups need. `pip install parseforge[sampling]` adds Netmiko for live device
39
- sampling; combine extras as needed, e.g.
40
- `pip install parseforge[anthropic,deepseek,sampling]`.
36
+ extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
37
+ `groq`, `xai`, `together`, `fireworks`, `perplexity`, `openrouter`,
38
+ `moonshot`, or `cerebras`. `--provider` defaults to `anthropic` wherever it
39
+ isn't required, so that's the one most setups need. `pip install
40
+ parseforge[sampling]` adds Netmiko for live device sampling; combine extras
41
+ as needed, e.g.
42
+ `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,sampling]`.
41
43
 
42
44
  ## Development
43
45
 
@@ -45,9 +47,11 @@ sampling; combine extras as needed, e.g.
45
47
  pip install -e ".[dev,sampling]"
46
48
  pytest
47
49
  ```
48
- `dev` already includes both `anthropic` and `openai` (tests exercise both
49
- providers, never silently skip) add `,anthropic`/`,deepseek` explicitly only
50
- if installing outside of `dev`.
50
+ `dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
51
+ all eleven providers`anthropic`, `openai`, `deepseek`, `groq`, `xai`,
52
+ `together`, `fireworks`, `perplexity`, `openrouter`, `moonshot`, `cerebras`
53
+ share just those two packages — and never silently skip) — add the specific
54
+ `,<provider>` extra explicitly only if installing outside of `dev`.
51
55
 
52
56
  Linting/formatting/type-checking/docs run through tox instead of extras — see
53
57
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -69,8 +73,8 @@ parseforge name --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.
69
73
  ```
70
74
  Still needs an LLM provider on a cache miss — `--provider` defaults to `anthropic`,
71
75
  and `--api-key` falls back to that provider's own env var (`ANTHROPIC_API_KEY`/
72
- `DEEPSEEK_API_KEY`). A cache hit (a command already seen before) never touches the
73
- LLM, so no key is needed at all in that case.
76
+ `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
77
+ never touches the LLM, so no key is needed at all in that case.
74
78
 
75
79
  **`check`** — validate a connector or provider before spending time/tokens on a real
76
80
  run. With neither `--env` nor explicit connection flags, prints what a connector needs
@@ -0,0 +1 @@
1
+ __version__ = "0.2.6"
@@ -23,7 +23,16 @@ from parseforge.cli import config as cli_config
23
23
 
24
24
  _BUILDERS: dict[str, type[naming.RegexBuilder]] = {
25
25
  "anthropic": naming.AnthropicRegexBuilder,
26
+ "cerebras": naming.CerebrasRegexBuilder,
26
27
  "deepseek": naming.DeepSeekRegexBuilder,
28
+ "fireworks": naming.FireworksRegexBuilder,
29
+ "groq": naming.GroqRegexBuilder,
30
+ "moonshot": naming.MoonshotRegexBuilder,
31
+ "openai": naming.OpenAIRegexBuilder,
32
+ "openrouter": naming.OpenRouterRegexBuilder,
33
+ "perplexity": naming.PerplexityRegexBuilder,
34
+ "together": naming.TogetherRegexBuilder,
35
+ "xai": naming.XAIRegexBuilder,
27
36
  }
28
37
 
29
38
  _CONNECTORS = ("netmiko",)
@@ -198,7 +207,10 @@ def main() -> None:
198
207
  "--api-key",
199
208
  default=None,
200
209
  help="Provider API key. Defaults to that provider's own API key environment "
201
- "variable (ANTHROPIC_API_KEY, DEEPSEEK_API_KEY); only needed on a cache miss.",
210
+ "variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GROQ_API_KEY, "
211
+ "XAI_API_KEY, TOGETHER_API_KEY, FIREWORKS_API_KEY, PERPLEXITY_API_KEY, "
212
+ "OPENROUTER_API_KEY, MOONSHOT_API_KEY, CEREBRAS_API_KEY); only needed on a "
213
+ "cache miss.",
202
214
  )
203
215
  @click.option(
204
216
  "--model",
@@ -266,8 +278,9 @@ def name_cmd(
266
278
  "--provider",
267
279
  required=True,
268
280
  help="LLM provider for template generation (textfsm-ai's own registry, "
269
- 'e.g. "anthropic", "deepseek"). Naming uses its own separate '
270
- "--naming-provider, not this one.",
281
+ 'e.g. "anthropic", "openai", "deepseek", "groq", "xai", "together", '
282
+ '"fireworks", "perplexity", "openrouter", "moonshot", "cerebras"). Naming '
283
+ "uses its own separate --naming-provider, not this one.",
271
284
  )
272
285
  @click.option("--api-key", required=True, help="API key for the generation LLM call.")
273
286
  @click.option("--model", required=True, help="Model for the generation LLM call.")
@@ -8,7 +8,19 @@ from .llm import (
8
8
  UnimplementedRegexBuilder,
9
9
  build_prompt,
10
10
  )
11
- from .providers import AnthropicRegexBuilder, DeepSeekRegexBuilder
11
+ from .providers import (
12
+ AnthropicRegexBuilder,
13
+ CerebrasRegexBuilder,
14
+ DeepSeekRegexBuilder,
15
+ FireworksRegexBuilder,
16
+ GroqRegexBuilder,
17
+ MoonshotRegexBuilder,
18
+ OpenAIRegexBuilder,
19
+ OpenRouterRegexBuilder,
20
+ PerplexityRegexBuilder,
21
+ TogetherRegexBuilder,
22
+ XAIRegexBuilder,
23
+ )
12
24
  from .resolver import NamingResolution, cli_name, resolve_cli_name
13
25
 
14
26
  __all__ = [
@@ -19,7 +31,16 @@ __all__ = [
19
31
  "RegexBuilder",
20
32
  "UnimplementedRegexBuilder",
21
33
  "AnthropicRegexBuilder",
34
+ "CerebrasRegexBuilder",
22
35
  "DeepSeekRegexBuilder",
36
+ "FireworksRegexBuilder",
37
+ "GroqRegexBuilder",
38
+ "MoonshotRegexBuilder",
39
+ "OpenAIRegexBuilder",
40
+ "OpenRouterRegexBuilder",
41
+ "PerplexityRegexBuilder",
42
+ "TogetherRegexBuilder",
43
+ "XAIRegexBuilder",
23
44
  "LLMCLIResponse",
24
45
  "TokenUsage",
25
46
  "build_prompt",
@@ -0,0 +1,25 @@
1
+ from .anthropic import AnthropicRegexBuilder
2
+ from .cerebras import CerebrasRegexBuilder
3
+ from .deepseek import DeepSeekRegexBuilder
4
+ from .fireworks import FireworksRegexBuilder
5
+ from .groq import GroqRegexBuilder
6
+ from .moonshot import MoonshotRegexBuilder
7
+ from .openai import OpenAIRegexBuilder
8
+ from .openrouter import OpenRouterRegexBuilder
9
+ from .perplexity import PerplexityRegexBuilder
10
+ from .together import TogetherRegexBuilder
11
+ from .xai import XAIRegexBuilder
12
+
13
+ __all__ = [
14
+ "AnthropicRegexBuilder",
15
+ "CerebrasRegexBuilder",
16
+ "DeepSeekRegexBuilder",
17
+ "FireworksRegexBuilder",
18
+ "GroqRegexBuilder",
19
+ "MoonshotRegexBuilder",
20
+ "OpenAIRegexBuilder",
21
+ "OpenRouterRegexBuilder",
22
+ "PerplexityRegexBuilder",
23
+ "TogetherRegexBuilder",
24
+ "XAIRegexBuilder",
25
+ ]
@@ -0,0 +1,128 @@
1
+ """Cerebras-backed RegexBuilder implementation.
2
+
3
+ Cerebras' API mirrors OpenAI's chat.completions surface (same request/
4
+ response format), so this uses the `openai` SDK pointed at Cerebras'
5
+ base URL rather than a dedicated Cerebras client.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import time
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
15
+ from .cost import estimate_cost
16
+ from .errors import format_llm_error_reason, is_retryable
17
+ from .models import default_model
18
+ from .text import extract_pattern
19
+
20
+ if TYPE_CHECKING:
21
+ import openai as openai_sdk
22
+
23
+ DEFAULT_MODEL = default_model("cerebras")
24
+
25
+ _BASE_URL = "https://api.cerebras.ai/v1"
26
+
27
+ _DEFAULT_MAX_TOKENS = 1024
28
+
29
+
30
+ def _import_openai() -> Any:
31
+ """Deferred import — openai is an optional extra (parseforge[cerebras]);
32
+ nothing else in this module (or a cache-hit lookup, which never reaches
33
+ build_pattern at all — see resolver.cli_name) should require it to be
34
+ installed."""
35
+ try:
36
+ import openai
37
+ except ImportError as exc:
38
+ raise ImportError(
39
+ "the openai package is required to use CerebrasRegexBuilder — "
40
+ "install it via `pip install parseforge[cerebras]`"
41
+ ) from exc
42
+ return openai
43
+
44
+
45
+ class CerebrasRegexBuilder:
46
+ """Builds a cli-name regex pattern by prompting a Cerebras-hosted model.
47
+
48
+ The client is constructed lazily, on the first actual call — not in
49
+ ``__init__`` — so this can be used as a default RegexBuilder without
50
+ requiring ``CEREBRAS_API_KEY`` to be set for cache-hit lookups, which
51
+ never reach the LLM at all (see resolver.cli_name).
52
+
53
+ API key resolves from the ``api_key`` argument if given, otherwise the
54
+ ``CEREBRAS_API_KEY`` environment variable (unlike Anthropic/OpenAI's
55
+ own SDKs, the ``openai`` package has no built-in notion of Cerebras'
56
+ key, so this is resolved explicitly rather than left to the SDK).
57
+ """
58
+
59
+ provider = "cerebras"
60
+
61
+ def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
62
+ self.model = model
63
+ self._api_key = api_key
64
+ self._client: openai_sdk.OpenAI | None = None
65
+
66
+ def _get_client(self) -> openai_sdk.OpenAI:
67
+ if self._client is None:
68
+ openai = _import_openai()
69
+ api_key = self._api_key or os.environ.get("CEREBRAS_API_KEY")
70
+ if not api_key:
71
+ raise RuntimeError(
72
+ "no Cerebras API key — pass api_key or set CEREBRAS_API_KEY"
73
+ )
74
+ self._client = openai.OpenAI(api_key=api_key, base_url=_BASE_URL)
75
+ return self._client
76
+
77
+ def build_pattern(
78
+ self, command: str, context: CliContext, **kwargs: Any
79
+ ) -> LLMCLIResponse:
80
+ openai = _import_openai()
81
+ prompt = build_prompt(command, context)
82
+ max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
83
+
84
+ start = time.monotonic()
85
+ try:
86
+ response = self._get_client().chat.completions.create(
87
+ model=self.model,
88
+ max_tokens=max_tokens,
89
+ messages=[{"role": "user", "content": prompt}],
90
+ **kwargs,
91
+ )
92
+ except openai.OpenAIError as exc:
93
+ if not is_retryable(exc):
94
+ # Same request would fail the same way again — stop rather
95
+ # than let a caller burn another attempt on it.
96
+ raise
97
+ return LLMCLIResponse(
98
+ content="",
99
+ raw=exc,
100
+ usage=TokenUsage(
101
+ input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
102
+ ),
103
+ duration_ms=(time.monotonic() - start) * 1000,
104
+ reason=format_llm_error_reason(exc),
105
+ ready=False,
106
+ )
107
+ duration_ms = (time.monotonic() - start) * 1000
108
+
109
+ choice = response.choices[0]
110
+ return LLMCLIResponse(
111
+ content=extract_pattern(choice.message.content or ""),
112
+ raw=response,
113
+ usage=TokenUsage(
114
+ input_tokens=response.usage.prompt_tokens,
115
+ output_tokens=response.usage.completion_tokens,
116
+ total_tokens=response.usage.total_tokens,
117
+ estimated_cost=estimate_cost(
118
+ input_tokens=response.usage.prompt_tokens,
119
+ output_tokens=response.usage.completion_tokens,
120
+ total_tokens=response.usage.total_tokens,
121
+ provider=self.provider,
122
+ model=self.model,
123
+ ),
124
+ ),
125
+ duration_ms=duration_ms,
126
+ reason=choice.finish_reason or "",
127
+ ready=choice.finish_reason == "stop",
128
+ )
@@ -0,0 +1,128 @@
1
+ """Fireworks AI-backed RegexBuilder implementation.
2
+
3
+ Fireworks' API mirrors OpenAI's chat.completions surface (same request/
4
+ response format), so this uses the `openai` SDK pointed at Fireworks'
5
+ base URL rather than a dedicated Fireworks client.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import time
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
15
+ from .cost import estimate_cost
16
+ from .errors import format_llm_error_reason, is_retryable
17
+ from .models import default_model
18
+ from .text import extract_pattern
19
+
20
+ if TYPE_CHECKING:
21
+ import openai as openai_sdk
22
+
23
+ DEFAULT_MODEL = default_model("fireworks")
24
+
25
+ _BASE_URL = "https://api.fireworks.ai/inference/v1"
26
+
27
+ _DEFAULT_MAX_TOKENS = 1024
28
+
29
+
30
+ def _import_openai() -> Any:
31
+ """Deferred import — openai is an optional extra (parseforge[fireworks]);
32
+ nothing else in this module (or a cache-hit lookup, which never reaches
33
+ build_pattern at all — see resolver.cli_name) should require it to be
34
+ installed."""
35
+ try:
36
+ import openai
37
+ except ImportError as exc:
38
+ raise ImportError(
39
+ "the openai package is required to use FireworksRegexBuilder — "
40
+ "install it via `pip install parseforge[fireworks]`"
41
+ ) from exc
42
+ return openai
43
+
44
+
45
+ class FireworksRegexBuilder:
46
+ """Builds a cli-name regex pattern by prompting a Fireworks-hosted model.
47
+
48
+ The client is constructed lazily, on the first actual call — not in
49
+ ``__init__`` — so this can be used as a default RegexBuilder without
50
+ requiring ``FIREWORKS_API_KEY`` to be set for cache-hit lookups, which
51
+ never reach the LLM at all (see resolver.cli_name).
52
+
53
+ API key resolves from the ``api_key`` argument if given, otherwise the
54
+ ``FIREWORKS_API_KEY`` environment variable (unlike Anthropic/OpenAI's
55
+ own SDKs, the ``openai`` package has no built-in notion of Fireworks'
56
+ key, so this is resolved explicitly rather than left to the SDK).
57
+ """
58
+
59
+ provider = "fireworks"
60
+
61
+ def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
62
+ self.model = model
63
+ self._api_key = api_key
64
+ self._client: openai_sdk.OpenAI | None = None
65
+
66
+ def _get_client(self) -> openai_sdk.OpenAI:
67
+ if self._client is None:
68
+ openai = _import_openai()
69
+ api_key = self._api_key or os.environ.get("FIREWORKS_API_KEY")
70
+ if not api_key:
71
+ raise RuntimeError(
72
+ "no Fireworks API key — pass api_key or set FIREWORKS_API_KEY"
73
+ )
74
+ self._client = openai.OpenAI(api_key=api_key, base_url=_BASE_URL)
75
+ return self._client
76
+
77
+ def build_pattern(
78
+ self, command: str, context: CliContext, **kwargs: Any
79
+ ) -> LLMCLIResponse:
80
+ openai = _import_openai()
81
+ prompt = build_prompt(command, context)
82
+ max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
83
+
84
+ start = time.monotonic()
85
+ try:
86
+ response = self._get_client().chat.completions.create(
87
+ model=self.model,
88
+ max_tokens=max_tokens,
89
+ messages=[{"role": "user", "content": prompt}],
90
+ **kwargs,
91
+ )
92
+ except openai.OpenAIError as exc:
93
+ if not is_retryable(exc):
94
+ # Same request would fail the same way again — stop rather
95
+ # than let a caller burn another attempt on it.
96
+ raise
97
+ return LLMCLIResponse(
98
+ content="",
99
+ raw=exc,
100
+ usage=TokenUsage(
101
+ input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
102
+ ),
103
+ duration_ms=(time.monotonic() - start) * 1000,
104
+ reason=format_llm_error_reason(exc),
105
+ ready=False,
106
+ )
107
+ duration_ms = (time.monotonic() - start) * 1000
108
+
109
+ choice = response.choices[0]
110
+ return LLMCLIResponse(
111
+ content=extract_pattern(choice.message.content or ""),
112
+ raw=response,
113
+ usage=TokenUsage(
114
+ input_tokens=response.usage.prompt_tokens,
115
+ output_tokens=response.usage.completion_tokens,
116
+ total_tokens=response.usage.total_tokens,
117
+ estimated_cost=estimate_cost(
118
+ input_tokens=response.usage.prompt_tokens,
119
+ output_tokens=response.usage.completion_tokens,
120
+ total_tokens=response.usage.total_tokens,
121
+ provider=self.provider,
122
+ model=self.model,
123
+ ),
124
+ ),
125
+ duration_ms=duration_ms,
126
+ reason=choice.finish_reason or "",
127
+ ready=choice.finish_reason == "stop",
128
+ )
@@ -0,0 +1,126 @@
1
+ """Groq-backed RegexBuilder implementation.
2
+
3
+ Groq's API mirrors OpenAI's chat.completions surface (same request/
4
+ response format), so this uses the `openai` SDK pointed at Groq's base
5
+ URL rather than a dedicated Groq client.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import time
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
15
+ from .cost import estimate_cost
16
+ from .errors import format_llm_error_reason, is_retryable
17
+ from .models import default_model
18
+ from .text import extract_pattern
19
+
20
+ if TYPE_CHECKING:
21
+ import openai as openai_sdk
22
+
23
+ DEFAULT_MODEL = default_model("groq")
24
+
25
+ _BASE_URL = "https://api.groq.com/openai/v1"
26
+
27
+ _DEFAULT_MAX_TOKENS = 1024
28
+
29
+
30
+ def _import_openai() -> Any:
31
+ """Deferred import — openai is an optional extra (parseforge[groq]);
32
+ nothing else in this module (or a cache-hit lookup, which never reaches
33
+ build_pattern at all — see resolver.cli_name) should require it to be
34
+ installed."""
35
+ try:
36
+ import openai
37
+ except ImportError as exc:
38
+ raise ImportError(
39
+ "the openai package is required to use GroqRegexBuilder — "
40
+ "install it via `pip install parseforge[groq]`"
41
+ ) from exc
42
+ return openai
43
+
44
+
45
+ class GroqRegexBuilder:
46
+ """Builds a cli-name regex pattern by prompting a Groq-hosted model.
47
+
48
+ The client is constructed lazily, on the first actual call — not in
49
+ ``__init__`` — so this can be used as a default RegexBuilder without
50
+ requiring ``GROQ_API_KEY`` to be set for cache-hit lookups, which
51
+ never reach the LLM at all (see resolver.cli_name).
52
+
53
+ API key resolves from the ``api_key`` argument if given, otherwise the
54
+ ``GROQ_API_KEY`` environment variable (unlike Anthropic/OpenAI's own
55
+ SDKs, the ``openai`` package has no built-in notion of Groq's key, so
56
+ this is resolved explicitly rather than left to the SDK).
57
+ """
58
+
59
+ provider = "groq"
60
+
61
+ def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
62
+ self.model = model
63
+ self._api_key = api_key
64
+ self._client: openai_sdk.OpenAI | None = None
65
+
66
+ def _get_client(self) -> openai_sdk.OpenAI:
67
+ if self._client is None:
68
+ openai = _import_openai()
69
+ api_key = self._api_key or os.environ.get("GROQ_API_KEY")
70
+ if not api_key:
71
+ raise RuntimeError("no Groq API key — pass api_key or set GROQ_API_KEY")
72
+ self._client = openai.OpenAI(api_key=api_key, base_url=_BASE_URL)
73
+ return self._client
74
+
75
+ def build_pattern(
76
+ self, command: str, context: CliContext, **kwargs: Any
77
+ ) -> LLMCLIResponse:
78
+ openai = _import_openai()
79
+ prompt = build_prompt(command, context)
80
+ max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
81
+
82
+ start = time.monotonic()
83
+ try:
84
+ response = self._get_client().chat.completions.create(
85
+ model=self.model,
86
+ max_tokens=max_tokens,
87
+ messages=[{"role": "user", "content": prompt}],
88
+ **kwargs,
89
+ )
90
+ except openai.OpenAIError as exc:
91
+ if not is_retryable(exc):
92
+ # Same request would fail the same way again — stop rather
93
+ # than let a caller burn another attempt on it.
94
+ raise
95
+ return LLMCLIResponse(
96
+ content="",
97
+ raw=exc,
98
+ usage=TokenUsage(
99
+ input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
100
+ ),
101
+ duration_ms=(time.monotonic() - start) * 1000,
102
+ reason=format_llm_error_reason(exc),
103
+ ready=False,
104
+ )
105
+ duration_ms = (time.monotonic() - start) * 1000
106
+
107
+ choice = response.choices[0]
108
+ return LLMCLIResponse(
109
+ content=extract_pattern(choice.message.content or ""),
110
+ raw=response,
111
+ usage=TokenUsage(
112
+ input_tokens=response.usage.prompt_tokens,
113
+ output_tokens=response.usage.completion_tokens,
114
+ total_tokens=response.usage.total_tokens,
115
+ estimated_cost=estimate_cost(
116
+ input_tokens=response.usage.prompt_tokens,
117
+ output_tokens=response.usage.completion_tokens,
118
+ total_tokens=response.usage.total_tokens,
119
+ provider=self.provider,
120
+ model=self.model,
121
+ ),
122
+ ),
123
+ duration_ms=duration_ms,
124
+ reason=choice.finish_reason or "",
125
+ ready=choice.finish_reason == "stop",
126
+ )