parseforge 0.2.5__tar.gz → 0.2.8__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 (58) hide show
  1. {parseforge-0.2.5/parseforge.egg-info → parseforge-0.2.8}/PKG-INFO +47 -10
  2. {parseforge-0.2.5 → parseforge-0.2.8}/README.md +14 -9
  3. parseforge-0.2.8/parseforge/__init__.py +1 -0
  4. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/cli/main.py +18 -4
  5. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/__init__.py +25 -1
  6. parseforge-0.2.8/parseforge/naming/providers/__init__.py +29 -0
  7. parseforge-0.2.8/parseforge/naming/providers/cerebras.py +128 -0
  8. parseforge-0.2.8/parseforge/naming/providers/cohere.py +173 -0
  9. parseforge-0.2.8/parseforge/naming/providers/fireworks.py +128 -0
  10. parseforge-0.2.8/parseforge/naming/providers/groq.py +126 -0
  11. parseforge-0.2.8/parseforge/naming/providers/mistral.py +163 -0
  12. parseforge-0.2.8/parseforge/naming/providers/models.yaml +131 -0
  13. parseforge-0.2.8/parseforge/naming/providers/moonshot.py +130 -0
  14. parseforge-0.2.8/parseforge/naming/providers/openrouter.py +130 -0
  15. parseforge-0.2.8/parseforge/naming/providers/perplexity.py +128 -0
  16. parseforge-0.2.8/parseforge/naming/providers/together.py +128 -0
  17. parseforge-0.2.8/parseforge/naming/providers/xai.py +126 -0
  18. {parseforge-0.2.5 → parseforge-0.2.8/parseforge.egg-info}/PKG-INFO +47 -10
  19. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge.egg-info/SOURCES.txt +10 -0
  20. parseforge-0.2.8/parseforge.egg-info/requires.txt +74 -0
  21. {parseforge-0.2.5 → parseforge-0.2.8}/pyproject.toml +22 -4
  22. parseforge-0.2.5/parseforge/__init__.py +0 -1
  23. parseforge-0.2.5/parseforge/naming/providers/__init__.py +0 -5
  24. parseforge-0.2.5/parseforge/naming/providers/models.yaml +0 -33
  25. parseforge-0.2.5/parseforge.egg-info/requires.txt +0 -32
  26. {parseforge-0.2.5 → parseforge-0.2.8}/LICENSE +0 -0
  27. {parseforge-0.2.5 → parseforge-0.2.8}/MANIFEST.in +0 -0
  28. {parseforge-0.2.5 → parseforge-0.2.8}/SPEC.md +0 -0
  29. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/cli/__init__.py +0 -0
  30. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/cli/config.py +0 -0
  31. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/drift.py +0 -0
  32. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/generation.py +0 -0
  33. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/integration.py +0 -0
  34. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/assemble.py +0 -0
  35. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/cache.py +0 -0
  36. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/llm.py +0 -0
  37. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/prompts.py +0 -0
  38. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/prompts.yaml +0 -0
  39. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/anthropic.py +0 -0
  40. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/cost.py +0 -0
  41. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/deepseek.py +0 -0
  42. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/errors.py +0 -0
  43. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/models.py +0 -0
  44. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/openai.py +0 -0
  45. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/text.py +0 -0
  46. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/resolver.py +0 -0
  47. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/paths.py +0 -0
  48. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/pipeline.py +0 -0
  49. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/promotion.py +0 -0
  50. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/sampling/__init__.py +0 -0
  51. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/sampling/backends/__init__.py +0 -0
  52. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/sampling/backends/netmiko.py +0 -0
  53. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/sampling/core.py +0 -0
  54. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/validation.py +0 -0
  55. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge.egg-info/dependency_links.txt +0 -0
  56. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge.egg-info/entry_points.txt +0 -0
  57. {parseforge-0.2.5 → parseforge-0.2.8}/parseforge.egg-info/top_level.txt +0 -0
  58. {parseforge-0.2.5 → parseforge-0.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parseforge
3
- Version: 0.2.5
3
+ Version: 0.2.8
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>
@@ -36,6 +36,36 @@ Requires-Dist: textfsm-ai[deepseek]>=0.6.1; extra == "deepseek"
36
36
  Provides-Extra: openai
37
37
  Requires-Dist: openai>=1.0.0; extra == "openai"
38
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"
63
+ Provides-Extra: mistral
64
+ Requires-Dist: mistralai==1.10.0; extra == "mistral"
65
+ Requires-Dist: textfsm-ai[mistral]>=0.6.1; extra == "mistral"
66
+ Provides-Extra: cohere
67
+ Requires-Dist: cohere==5.21.1; extra == "cohere"
68
+ Requires-Dist: textfsm-ai[cohere]>=0.6.1; extra == "cohere"
39
69
  Provides-Extra: sampling
40
70
  Requires-Dist: netmiko>=4.0; extra == "sampling"
41
71
  Provides-Extra: dev
@@ -46,6 +76,8 @@ Requires-Dist: black>=24.0.0; extra == "dev"
46
76
  Requires-Dist: mypy>=1.10.0; extra == "dev"
47
77
  Requires-Dist: anthropic>=0.40.0; extra == "dev"
48
78
  Requires-Dist: openai>=1.0.0; extra == "dev"
79
+ Requires-Dist: mistralai==1.10.0; extra == "dev"
80
+ Requires-Dist: cohere==5.21.1; extra == "dev"
49
81
  Provides-Extra: release
50
82
  Requires-Dist: bump2version; extra == "release"
51
83
  Requires-Dist: build; extra == "release"
@@ -86,11 +118,13 @@ pip install parseforge[anthropic]
86
118
  command that's pure local processing (`canonical`/`readable`/`recognizers`,
87
119
  `integration`, `promotion`) works with nothing further. Anything that calls an
88
120
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
89
- extra for whichever provider it uses: `anthropic`, `openai`, or `deepseek`.
90
- `--provider` defaults to `anthropic` wherever it isn't required, so that's the
91
- one most setups need. `pip install parseforge[sampling]` adds Netmiko for live
92
- device sampling; combine extras as needed, e.g.
93
- `pip install parseforge[anthropic,openai,deepseek,sampling]`.
121
+ extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
122
+ `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]`.
94
128
 
95
129
  ## Development
96
130
 
@@ -98,10 +132,13 @@ device sampling; combine extras as needed, e.g.
98
132
  pip install -e ".[dev,sampling]"
99
133
  pytest
100
134
  ```
101
- `dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
102
- all three providers — `anthropic`, `openai`, `deepseek` share just those two
103
- packages and never silently skip) — add `,anthropic`/`,openai`/`,deepseek`
104
- explicitly only if installing outside of `dev`.
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`.
105
142
 
106
143
  Linting/formatting/type-checking/docs run through tox instead of extras — see
107
144
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -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`, `openai`, or `deepseek`.
37
- `--provider` defaults to `anthropic` wherever it isn't required, so that's the
38
- one most setups need. `pip install parseforge[sampling]` adds Netmiko for live
39
- device sampling; combine extras as needed, e.g.
40
- `pip install parseforge[anthropic,openai,deepseek,sampling]`.
36
+ extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
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]`.
41
43
 
42
44
  ## Development
43
45
 
@@ -45,10 +47,13 @@ device sampling; combine extras as needed, e.g.
45
47
  pip install -e ".[dev,sampling]"
46
48
  pytest
47
49
  ```
48
- `dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
49
- all three providers — `anthropic`, `openai`, `deepseek` share just those two
50
- packages and never silently skip) — add `,anthropic`/`,openai`/`,deepseek`
51
- explicitly only if installing outside of `dev`.
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`.
52
57
 
53
58
  Linting/formatting/type-checking/docs run through tox instead of extras — see
54
59
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -0,0 +1 @@
1
+ __version__ = "0.2.8"
@@ -23,8 +23,18 @@ 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,
27
+ "cohere": naming.CohereRegexBuilder,
26
28
  "deepseek": naming.DeepSeekRegexBuilder,
29
+ "fireworks": naming.FireworksRegexBuilder,
30
+ "groq": naming.GroqRegexBuilder,
31
+ "mistral": naming.MistralRegexBuilder,
32
+ "moonshot": naming.MoonshotRegexBuilder,
27
33
  "openai": naming.OpenAIRegexBuilder,
34
+ "openrouter": naming.OpenRouterRegexBuilder,
35
+ "perplexity": naming.PerplexityRegexBuilder,
36
+ "together": naming.TogetherRegexBuilder,
37
+ "xai": naming.XAIRegexBuilder,
28
38
  }
29
39
 
30
40
  _CONNECTORS = ("netmiko",)
@@ -199,8 +209,10 @@ def main() -> None:
199
209
  "--api-key",
200
210
  default=None,
201
211
  help="Provider API key. Defaults to that provider's own API key environment "
202
- "variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY); only needed "
203
- "on a cache miss.",
212
+ "variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GROQ_API_KEY, "
213
+ "XAI_API_KEY, TOGETHER_API_KEY, FIREWORKS_API_KEY, PERPLEXITY_API_KEY, "
214
+ "OPENROUTER_API_KEY, MOONSHOT_API_KEY, CEREBRAS_API_KEY, MISTRAL_API_KEY, "
215
+ "COHERE_API_KEY); only needed on a cache miss.",
204
216
  )
205
217
  @click.option(
206
218
  "--model",
@@ -268,8 +280,10 @@ def name_cmd(
268
280
  "--provider",
269
281
  required=True,
270
282
  help="LLM provider for template generation (textfsm-ai's own registry, "
271
- 'e.g. "anthropic", "openai", "deepseek"). Naming uses its own separate '
272
- "--naming-provider, not this one.",
283
+ 'e.g. "anthropic", "openai", "deepseek", "groq", "xai", "together", '
284
+ '"fireworks", "perplexity", "openrouter", "moonshot", "cerebras", '
285
+ '"mistral", "cohere"). Naming uses its own separate --naming-provider, '
286
+ "not this one.",
273
287
  )
274
288
  @click.option("--api-key", required=True, help="API key for the generation LLM call.")
275
289
  @click.option("--model", required=True, help="Model for the generation LLM call.")
@@ -8,7 +8,21 @@ from .llm import (
8
8
  UnimplementedRegexBuilder,
9
9
  build_prompt,
10
10
  )
11
- from .providers import AnthropicRegexBuilder, DeepSeekRegexBuilder, OpenAIRegexBuilder
11
+ from .providers import (
12
+ AnthropicRegexBuilder,
13
+ CerebrasRegexBuilder,
14
+ CohereRegexBuilder,
15
+ DeepSeekRegexBuilder,
16
+ FireworksRegexBuilder,
17
+ GroqRegexBuilder,
18
+ MistralRegexBuilder,
19
+ MoonshotRegexBuilder,
20
+ OpenAIRegexBuilder,
21
+ OpenRouterRegexBuilder,
22
+ PerplexityRegexBuilder,
23
+ TogetherRegexBuilder,
24
+ XAIRegexBuilder,
25
+ )
12
26
  from .resolver import NamingResolution, cli_name, resolve_cli_name
13
27
 
14
28
  __all__ = [
@@ -19,8 +33,18 @@ __all__ = [
19
33
  "RegexBuilder",
20
34
  "UnimplementedRegexBuilder",
21
35
  "AnthropicRegexBuilder",
36
+ "CerebrasRegexBuilder",
37
+ "CohereRegexBuilder",
22
38
  "DeepSeekRegexBuilder",
39
+ "FireworksRegexBuilder",
40
+ "GroqRegexBuilder",
41
+ "MistralRegexBuilder",
42
+ "MoonshotRegexBuilder",
23
43
  "OpenAIRegexBuilder",
44
+ "OpenRouterRegexBuilder",
45
+ "PerplexityRegexBuilder",
46
+ "TogetherRegexBuilder",
47
+ "XAIRegexBuilder",
24
48
  "LLMCLIResponse",
25
49
  "TokenUsage",
26
50
  "build_prompt",
@@ -0,0 +1,29 @@
1
+ from .anthropic import AnthropicRegexBuilder
2
+ from .cerebras import CerebrasRegexBuilder
3
+ from .cohere import CohereRegexBuilder
4
+ from .deepseek import DeepSeekRegexBuilder
5
+ from .fireworks import FireworksRegexBuilder
6
+ from .groq import GroqRegexBuilder
7
+ from .mistral import MistralRegexBuilder
8
+ from .moonshot import MoonshotRegexBuilder
9
+ from .openai import OpenAIRegexBuilder
10
+ from .openrouter import OpenRouterRegexBuilder
11
+ from .perplexity import PerplexityRegexBuilder
12
+ from .together import TogetherRegexBuilder
13
+ from .xai import XAIRegexBuilder
14
+
15
+ __all__ = [
16
+ "AnthropicRegexBuilder",
17
+ "CerebrasRegexBuilder",
18
+ "CohereRegexBuilder",
19
+ "DeepSeekRegexBuilder",
20
+ "FireworksRegexBuilder",
21
+ "GroqRegexBuilder",
22
+ "MistralRegexBuilder",
23
+ "MoonshotRegexBuilder",
24
+ "OpenAIRegexBuilder",
25
+ "OpenRouterRegexBuilder",
26
+ "PerplexityRegexBuilder",
27
+ "TogetherRegexBuilder",
28
+ "XAIRegexBuilder",
29
+ ]
@@ -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,173 @@
1
+ """Cohere-backed RegexBuilder implementation.
2
+
3
+ Like Mistral, Cohere's official Python SDK is not built on the OpenAI
4
+ client library — it has its own generated client shape. Unlike Mistral's
5
+ single client (which exposes both complete()/complete_async() on one
6
+ object), Cohere exposes genuinely separate ``ClientV2``/``AsyncClientV2``
7
+ classes; this builder only ever needs the sync ``ClientV2``, since
8
+ ``RegexBuilder.build_pattern()`` is a synchronous call.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import time
15
+ from typing import TYPE_CHECKING, Any, cast
16
+
17
+ from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
18
+ from .cost import estimate_cost
19
+ from .models import default_model
20
+ from .text import extract_pattern
21
+
22
+ if TYPE_CHECKING:
23
+ import cohere as cohere_sdk
24
+
25
+ DEFAULT_MODEL = default_model("cohere")
26
+
27
+ _DEFAULT_MAX_TOKENS = 1024
28
+
29
+ # HTTP statuses where retrying the exact same request can't succeed —
30
+ # mirrors providers/errors.py's NON_RETRYABLE class-name set (BadRequest,
31
+ # Authentication, PermissionDenied, NotFound, Conflict, RequestTooLarge,
32
+ # UnprocessableEntity). The cohere SDK raises a family of named exception
33
+ # classes (BadRequestError, UnauthorizedError, ForbiddenError, ...), but
34
+ # those names don't line up with OpenAI/Anthropic's naming, and every one
35
+ # of them carries its status on exc.status_code (see
36
+ # cohere.core.api_error.ApiError, their shared base) — so classification
37
+ # here is done by status code instead of exception class name.
38
+ _NON_RETRYABLE_STATUSES = frozenset({400, 401, 403, 404, 409, 413, 422})
39
+
40
+
41
+ def _import_cohere() -> Any:
42
+ """Deferred import — cohere is an optional extra (parseforge[cohere]);
43
+ nothing else in this module (or a cache-hit lookup, which never reaches
44
+ build_pattern at all — see resolver.cli_name) should require it to be
45
+ installed."""
46
+ try:
47
+ import cohere
48
+ import cohere.core.api_error # noqa: F401 (makes cohere.core.api_error.ApiError reachable off the returned module)
49
+ except ImportError as exc:
50
+ raise ImportError(
51
+ "the cohere package is required to use CohereRegexBuilder — "
52
+ "install it via `pip install parseforge[cohere]`"
53
+ ) from exc
54
+ return cohere
55
+
56
+
57
+ def _status_code(exc: Any) -> int | None:
58
+ return getattr(exc, "status_code", None)
59
+
60
+
61
+ def _is_retryable(exc: Any) -> bool:
62
+ return _status_code(exc) not in _NON_RETRYABLE_STATUSES
63
+
64
+
65
+ def _format_error_reason(exc: Any) -> str:
66
+ return f"LLM-ERROR-cohere_sdk-{_status_code(exc)}-{exc}"
67
+
68
+
69
+ def _extract_text(content: Any) -> str:
70
+ """Cohere's AssistantMessageResponse.content is a list of content
71
+ blocks (text and/or thinking blocks), unlike Mistral/OpenAI's plain
72
+ string — join the text blocks' .text, same as textfsm-ai's own
73
+ CohereProvider._parse_cohere_response()."""
74
+ if isinstance(content, str):
75
+ return content
76
+ return "".join(getattr(block, "text", "") for block in content or [])
77
+
78
+
79
+ class CohereRegexBuilder:
80
+ """Builds a cli-name regex pattern by prompting a Cohere-hosted model.
81
+
82
+ The client is constructed lazily, on the first actual call — not in
83
+ ``__init__`` — so this can be used as a default RegexBuilder without
84
+ requiring ``COHERE_API_KEY`` to be set for cache-hit lookups, which
85
+ never reach the LLM at all (see resolver.cli_name).
86
+
87
+ API key resolves from the ``api_key`` argument if given, otherwise the
88
+ ``COHERE_API_KEY`` environment variable (the SDK's own default).
89
+ """
90
+
91
+ provider = "cohere"
92
+
93
+ def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
94
+ self.model = model
95
+ self._api_key = api_key
96
+ self._client: cohere_sdk.ClientV2 | None = None
97
+
98
+ def _get_client(self) -> cohere_sdk.ClientV2:
99
+ if self._client is None:
100
+ cohere = _import_cohere()
101
+ api_key = self._api_key or os.environ.get("COHERE_API_KEY")
102
+ if not api_key:
103
+ raise RuntimeError(
104
+ "no Cohere API key — pass api_key or set COHERE_API_KEY"
105
+ )
106
+ self._client = cohere.ClientV2(api_key=api_key)
107
+ return self._client
108
+
109
+ def build_pattern(
110
+ self, command: str, context: CliContext, **kwargs: Any
111
+ ) -> LLMCLIResponse:
112
+ cohere = _import_cohere()
113
+ prompt = build_prompt(command, context)
114
+ max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
115
+
116
+ start = time.monotonic()
117
+ try:
118
+ response = self._get_client().chat(
119
+ model=self.model,
120
+ max_tokens=max_tokens,
121
+ # cast: cohere's generated stubs want a precise
122
+ # UserChatMessageV2/... union or matching TypedDict, but a
123
+ # plain {"role": "user", "content": ...} dict is exactly
124
+ # that shape.
125
+ messages=cast(Any, [{"role": "user", "content": prompt}]),
126
+ **kwargs,
127
+ )
128
+ except cohere.core.api_error.ApiError as exc:
129
+ if not _is_retryable(exc):
130
+ # Same request would fail the same way again — stop rather
131
+ # than let a caller burn another attempt on it.
132
+ raise
133
+ return LLMCLIResponse(
134
+ content="",
135
+ raw=exc,
136
+ usage=TokenUsage(
137
+ input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
138
+ ),
139
+ duration_ms=(time.monotonic() - start) * 1000,
140
+ reason=_format_error_reason(exc),
141
+ ready=False,
142
+ )
143
+ duration_ms = (time.monotonic() - start) * 1000
144
+
145
+ text = _extract_text(response.message.content)
146
+ # usage/usage.tokens are both Optional, and input_tokens/
147
+ # output_tokens are typed float — unlike every openai-compat
148
+ # provider, Cohere's usage.tokens has no total_tokens field at
149
+ # all, so it's computed here rather than read off the response.
150
+ tokens = response.usage.tokens if response.usage else None
151
+ input_tokens = int(tokens.input_tokens or 0) if tokens else 0
152
+ output_tokens = int(tokens.output_tokens or 0) if tokens else 0
153
+ total_tokens = input_tokens + output_tokens
154
+
155
+ return LLMCLIResponse(
156
+ content=extract_pattern(text),
157
+ raw=response,
158
+ usage=TokenUsage(
159
+ input_tokens=input_tokens,
160
+ output_tokens=output_tokens,
161
+ total_tokens=total_tokens,
162
+ estimated_cost=estimate_cost(
163
+ input_tokens=input_tokens,
164
+ output_tokens=output_tokens,
165
+ total_tokens=total_tokens,
166
+ provider=self.provider,
167
+ model=self.model,
168
+ ),
169
+ ),
170
+ duration_ms=duration_ms,
171
+ reason=response.finish_reason or "",
172
+ ready=response.finish_reason == "COMPLETE",
173
+ )
@@ -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
+ )