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.
- {parseforge-0.2.5/parseforge.egg-info → parseforge-0.2.8}/PKG-INFO +47 -10
- {parseforge-0.2.5 → parseforge-0.2.8}/README.md +14 -9
- parseforge-0.2.8/parseforge/__init__.py +1 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/cli/main.py +18 -4
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/__init__.py +25 -1
- parseforge-0.2.8/parseforge/naming/providers/__init__.py +29 -0
- parseforge-0.2.8/parseforge/naming/providers/cerebras.py +128 -0
- parseforge-0.2.8/parseforge/naming/providers/cohere.py +173 -0
- parseforge-0.2.8/parseforge/naming/providers/fireworks.py +128 -0
- parseforge-0.2.8/parseforge/naming/providers/groq.py +126 -0
- parseforge-0.2.8/parseforge/naming/providers/mistral.py +163 -0
- parseforge-0.2.8/parseforge/naming/providers/models.yaml +131 -0
- parseforge-0.2.8/parseforge/naming/providers/moonshot.py +130 -0
- parseforge-0.2.8/parseforge/naming/providers/openrouter.py +130 -0
- parseforge-0.2.8/parseforge/naming/providers/perplexity.py +128 -0
- parseforge-0.2.8/parseforge/naming/providers/together.py +128 -0
- parseforge-0.2.8/parseforge/naming/providers/xai.py +126 -0
- {parseforge-0.2.5 → parseforge-0.2.8/parseforge.egg-info}/PKG-INFO +47 -10
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge.egg-info/SOURCES.txt +10 -0
- parseforge-0.2.8/parseforge.egg-info/requires.txt +74 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/pyproject.toml +22 -4
- parseforge-0.2.5/parseforge/__init__.py +0 -1
- parseforge-0.2.5/parseforge/naming/providers/__init__.py +0 -5
- parseforge-0.2.5/parseforge/naming/providers/models.yaml +0 -33
- parseforge-0.2.5/parseforge.egg-info/requires.txt +0 -32
- {parseforge-0.2.5 → parseforge-0.2.8}/LICENSE +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/MANIFEST.in +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/SPEC.md +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/cli/__init__.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/cli/config.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/drift.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/generation.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/integration.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/assemble.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/cache.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/llm.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/prompts.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/prompts.yaml +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/anthropic.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/cost.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/deepseek.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/errors.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/models.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/openai.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/providers/text.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/naming/resolver.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/paths.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/pipeline.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/promotion.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/sampling/__init__.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/sampling/backends/__init__.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/sampling/backends/netmiko.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/sampling/core.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge/validation.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge.egg-info/dependency_links.txt +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge.egg-info/entry_points.txt +0 -0
- {parseforge-0.2.5 → parseforge-0.2.8}/parseforge.egg-info/top_level.txt +0 -0
- {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.
|
|
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`,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`pip install parseforge[
|
|
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
|
|
102
|
-
all
|
|
103
|
-
|
|
104
|
-
|
|
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`,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
`pip install parseforge[
|
|
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
|
|
49
|
-
all
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
203
|
-
"
|
|
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"
|
|
272
|
-
"
|
|
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
|
|
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
|
+
)
|