arcus-cli 0.1.0__tar.gz → 0.2.1__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.
- arcus_cli-0.2.1/.github/ISSUE_TEMPLATE/bug_report.yml +47 -0
- arcus_cli-0.2.1/.github/ISSUE_TEMPLATE/feature_request.yml +25 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/.github/workflows/release.yml +13 -0
- arcus_cli-0.2.1/CONTRIBUTING.md +48 -0
- arcus_cli-0.2.1/PKG-INFO +496 -0
- arcus_cli-0.2.1/README.md +466 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/pyproject.toml +1 -1
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/adapters/arc_adapter.py +45 -2
- arcus_cli-0.2.1/src/arcus/cli.py +1078 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/config.py +19 -4
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/quality/gate.py +63 -9
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/bandit.py +22 -2
- arcus_cli-0.2.1/src/arcus/routing/model_catalog.py +96 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/reward.py +9 -13
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/warm_start.py +9 -6
- arcus_cli-0.2.1/tests/adapters/test_arc_adapter.py +167 -0
- arcus_cli-0.2.1/tests/quality/test_gate.py +402 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/routing/test_bandit.py +13 -2
- arcus_cli-0.2.1/tests/routing/test_model_catalog.py +116 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/routing/test_reward.py +10 -15
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/routing/test_warm_start.py +31 -1
- arcus_cli-0.2.1/tests/test_cli.py +1846 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/test_config.py +30 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/uv.lock +1 -1
- arcus_cli-0.1.0/PKG-INFO +0 -308
- arcus_cli-0.1.0/README.md +0 -278
- arcus_cli-0.1.0/src/arcus/cli.py +0 -335
- arcus_cli-0.1.0/tests/adapters/test_arc_adapter.py +0 -70
- arcus_cli-0.1.0/tests/quality/test_gate.py +0 -230
- arcus_cli-0.1.0/tests/test_cli.py +0 -472
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/.github/workflows/ci.yml +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/.gitignore +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/.python-version +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/LICENSE +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/__init__.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/adapters/__init__.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/cache/__init__.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/cache/benchmark.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/cache/semantic_cache.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/embeddings.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/eval/__init__.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/eval/offline.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/eval/regret.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/quality/__init__.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/__init__.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/context.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/storage/__init__.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/storage/db.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/storage/stats.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/adapters/test_arc_adapter_live.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/cache/test_benchmark.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/cache/test_semantic_cache.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/eval/test_offline.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/eval/test_regret.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/routing/test_context.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/storage/test_db.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/storage/test_stats.py +0 -0
- {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/test_embeddings.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something in arcus isn't working as expected
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what-happened
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened
|
|
9
|
+
description: What did you run, and what went wrong?
|
|
10
|
+
placeholder: |
|
|
11
|
+
Ran `arcus "..."` and got a traceback / wrong answer / hang, etc.
|
|
12
|
+
validations:
|
|
13
|
+
required: true
|
|
14
|
+
|
|
15
|
+
- type: textarea
|
|
16
|
+
id: expected
|
|
17
|
+
attributes:
|
|
18
|
+
label: What you expected instead
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
|
|
22
|
+
- type: input
|
|
23
|
+
id: version
|
|
24
|
+
attributes:
|
|
25
|
+
label: arcus-cli version
|
|
26
|
+
description: Output of `pip show arcus-cli` or `uv tool list`
|
|
27
|
+
validations:
|
|
28
|
+
required: true
|
|
29
|
+
|
|
30
|
+
- type: dropdown
|
|
31
|
+
id: mode
|
|
32
|
+
attributes:
|
|
33
|
+
label: Which mode were you using?
|
|
34
|
+
options:
|
|
35
|
+
- "arcus \"...\" (one-shot)"
|
|
36
|
+
- "arcus chat"
|
|
37
|
+
- "arcus stats"
|
|
38
|
+
- "arcus --random"
|
|
39
|
+
- Not sure / other
|
|
40
|
+
validations:
|
|
41
|
+
required: true
|
|
42
|
+
|
|
43
|
+
- type: textarea
|
|
44
|
+
id: logs
|
|
45
|
+
attributes:
|
|
46
|
+
label: Full error output, if any
|
|
47
|
+
render: shell
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest something arcus should do
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: What problem does this solve?
|
|
9
|
+
description: What are you trying to do that arcus doesn't support today?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: proposal
|
|
15
|
+
attributes:
|
|
16
|
+
label: What you'd like to see
|
|
17
|
+
description: A rough idea of the behavior or interface is fine, doesn't need to be a full spec.
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: alternatives
|
|
23
|
+
attributes:
|
|
24
|
+
label: Alternatives you've considered
|
|
25
|
+
description: Any workarounds you're using today, or other approaches you thought about.
|
|
@@ -48,3 +48,16 @@ jobs:
|
|
|
48
48
|
|
|
49
49
|
- name: Publish to PyPI
|
|
50
50
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
51
|
+
|
|
52
|
+
github_release:
|
|
53
|
+
needs: publish
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
permissions:
|
|
56
|
+
contents: write # required to create the GitHub Release
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
|
|
60
|
+
- name: Create GitHub Release
|
|
61
|
+
uses: softprops/action-gh-release@v2
|
|
62
|
+
with:
|
|
63
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Contributing to Arcus
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/ShakirFarhan/Arcus.git
|
|
7
|
+
cd Arcus
|
|
8
|
+
uv sync
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You'll need an ARC API key to run the tool live (`llm.arc.vt.edu` > User
|
|
12
|
+
profile > Settings > Account > API keys). Most of the test suite runs
|
|
13
|
+
without one; a handful of live tests in `tests/adapters/test_arc_adapter_live.py`
|
|
14
|
+
only run if `ARC_API_KEY` is set in your shell:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv run pytest tests/ -q
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Before opening a PR
|
|
21
|
+
|
|
22
|
+
- Add a test for any new business logic (routing, reward calculation,
|
|
23
|
+
cache scoring, quality checks). Bug fixes should include a test that
|
|
24
|
+
fails without the fix.
|
|
25
|
+
- Keep PRs focused, one change per PR rather than bundling unrelated
|
|
26
|
+
fixes together.
|
|
27
|
+
- Run the full test suite locally before pushing; CI runs it again on
|
|
28
|
+
every PR but catching it early saves a round trip.
|
|
29
|
+
- Algorithmic functions (bandit logic, reward math, cache scoring)
|
|
30
|
+
should have a docstring explaining *why* a design choice was made,
|
|
31
|
+
not just what the code does.
|
|
32
|
+
|
|
33
|
+
## Code style
|
|
34
|
+
|
|
35
|
+
- No new dependencies without discussing them in the PR description
|
|
36
|
+
first, this project deliberately keeps its dependency footprint
|
|
37
|
+
small.
|
|
38
|
+
- Comments should explain non-obvious reasoning (a workaround, a
|
|
39
|
+
constraint, a subtle invariant), not restate what the code already
|
|
40
|
+
says.
|
|
41
|
+
- Prefer editing existing files and reusing existing patterns over
|
|
42
|
+
introducing new abstractions.
|
|
43
|
+
|
|
44
|
+
## Reporting bugs
|
|
45
|
+
|
|
46
|
+
Open an issue with what you ran, what you expected, and what actually
|
|
47
|
+
happened. If it's reproducible with `arcus --random`, mention that too,
|
|
48
|
+
it helps narrow down whether it's routing-specific.
|
arcus_cli-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: arcus-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Adaptive routing, a quality gate, and a correctness-aware cache on top of Virginia Tech ARC's LLM API
|
|
5
|
+
Project-URL: Homepage, https://github.com/ShakirFarhan/Arcus
|
|
6
|
+
Project-URL: Repository, https://github.com/ShakirFarhan/Arcus
|
|
7
|
+
Project-URL: Issues, https://github.com/ShakirFarhan/Arcus/issues
|
|
8
|
+
Author: Shakir Farhan
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: arc,bandit,cli,llm,routing,virginia-tech
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Requires-Dist: openai
|
|
22
|
+
Requires-Dist: platformdirs>=4.11.3
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.15.0
|
|
24
|
+
Requires-Dist: pydantic>=2.13.4
|
|
25
|
+
Requires-Dist: rich>=15.0.0
|
|
26
|
+
Requires-Dist: sentence-transformers>=6.0.0
|
|
27
|
+
Requires-Dist: sqlmodel>=0.0.39
|
|
28
|
+
Requires-Dist: typer>=0.27.1
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Arcus
|
|
32
|
+
|
|
33
|
+
[](https://github.com/ShakirFarhan/Arcus/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/arcus-cli/)
|
|
35
|
+
[](https://pypi.org/project/arcus-cli/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
A CLI that sits on top of Virginia Tech ARC's LLM API and makes it
|
|
39
|
+
smarter: it picks which of ARC's four open-weight models to route a
|
|
40
|
+
request to, checks the response before handing it back to you, and
|
|
41
|
+
caches answers to questions it's already seen. Runs entirely on your own
|
|
42
|
+
machine with your own ARC key. Nothing goes through a shared server.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
arcus "explain how binary search works"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Contents:** [Why](#why) · [How it works](#how-it-works) · [Install](#install)
|
|
49
|
+
· [Usage](#usage) · [Status](#status) · [Security & privacy](#security--privacy)
|
|
50
|
+
|
|
51
|
+
## Why
|
|
52
|
+
|
|
53
|
+
ARC gives every VT user free access to four open-weight models
|
|
54
|
+
(gpt-oss-120b, GLM-5.3, Kimi-K3, DeepSeek-V4-Flash) through one
|
|
55
|
+
OpenAI-compatible endpoint ([ARC's own docs](https://www.docs.arc.vt.edu/ai/011_llm_api_arc_vt_edu.html)
|
|
56
|
+
cover the service itself, including its rate limits and data-handling
|
|
57
|
+
approval, arcus is a client built on top of it, not affiliated with
|
|
58
|
+
ARC). Picking a model by hand every time is
|
|
59
|
+
tedious, and a plain HTTP 200 doesn't tell you whether the response
|
|
60
|
+
inside it was actually any good, a truncated answer or a flat refusal
|
|
61
|
+
comes back looking the same as a correct one unless something reads the
|
|
62
|
+
content. Arcus adds three things on top of the raw API:
|
|
63
|
+
|
|
64
|
+
- **Adaptive routing** — a multi-armed bandit learns, per kind of
|
|
65
|
+
question, which model tends to give the best result for the least
|
|
66
|
+
latency and cost.
|
|
67
|
+
- **A quality gate** — validates every response (truncation, empty
|
|
68
|
+
output, repetition loops, refusal phrases, schema conformance) before
|
|
69
|
+
it reaches you, and silently retries with a different model if the
|
|
70
|
+
first one produced garbage.
|
|
71
|
+
- **A correctness-aware cache** — skips the API call entirely for
|
|
72
|
+
questions it's answered before, but only when it's actually confident
|
|
73
|
+
the new question means the same thing as the cached one.
|
|
74
|
+
|
|
75
|
+
## How it works
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
your question (CLI arg or piped stdin)
|
|
79
|
+
|
|
|
80
|
+
v
|
|
81
|
+
context classifier -- code / reasoning-math / writing / long-document / general
|
|
82
|
+
|
|
|
83
|
+
v
|
|
84
|
+
semantic cache check -- hit? return the cached answer, skip everything below
|
|
85
|
+
| miss
|
|
86
|
+
v
|
|
87
|
+
bandit router -- picks a model, one bandit instance per (task type, length) bucket
|
|
88
|
+
|
|
|
89
|
+
v
|
|
90
|
+
ARC API call (your own key, OpenAI-compatible endpoint)
|
|
91
|
+
|
|
|
92
|
+
v
|
|
93
|
+
quality gate -- validates the response, retries with a different model on failure
|
|
94
|
+
| pass
|
|
95
|
+
v
|
|
96
|
+
answer to you + write to cache + reward logged back to the bandit
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Everything after "your question" runs locally. The only network call
|
|
100
|
+
this tool ever makes is to ARC, with your own key.
|
|
101
|
+
|
|
102
|
+
### Context classification
|
|
103
|
+
|
|
104
|
+
Regex/keyword rules catch the obvious cases fast (a traceback is
|
|
105
|
+
obviously a code question, "write me a poem" is obviously a writing
|
|
106
|
+
request). Anything that doesn't match falls back to comparing the
|
|
107
|
+
prompt's embedding against a small set of labeled anchor examples per
|
|
108
|
+
category, so phrasing the regex rules never thought of still lands in
|
|
109
|
+
the right bucket instead of defaulting to "general." See
|
|
110
|
+
`src/arcus/routing/context.py`.
|
|
111
|
+
|
|
112
|
+
### Adaptive routing
|
|
113
|
+
|
|
114
|
+
Three interchangeable bandit algorithms, picked via config
|
|
115
|
+
(`bandit_algorithm` in `~/.config/arcus/config.toml`, default
|
|
116
|
+
`thompson`):
|
|
117
|
+
|
|
118
|
+
- **Epsilon-greedy** — simplest baseline, explores randomly a fixed
|
|
119
|
+
fraction of the time.
|
|
120
|
+
- **UCB1** — no tunable knob, explores under-tried arms automatically
|
|
121
|
+
via a confidence bound.
|
|
122
|
+
- **Thompson sampling** — Bayesian, samples from each arm's learned
|
|
123
|
+
`Beta` distribution, the default because it adapts fastest early on.
|
|
124
|
+
|
|
125
|
+
A random-selection baseline (`--random`) is also wired in as an A/B
|
|
126
|
+
comparison point, mostly useful for the offline evaluation report below.
|
|
127
|
+
The reward each arm is updated with is a weighted mix of quality (from
|
|
128
|
+
the gate below), normalized latency, and a simulated cost signal built
|
|
129
|
+
from real published hosting rates for these same open-weight models
|
|
130
|
+
(ARC itself is free, this exists to demonstrate cost-aware routing as a
|
|
131
|
+
practice). See `src/arcus/routing/bandit.py` and
|
|
132
|
+
`src/arcus/routing/reward.py`.
|
|
133
|
+
|
|
134
|
+
Since every `arcus` invocation is a fresh process, there's no daemon
|
|
135
|
+
holding the bandit's learned state in memory between runs. Instead,
|
|
136
|
+
`src/arcus/routing/warm_start.py` rebuilds it at the start of every call
|
|
137
|
+
by replaying the local request log, which works because a bandit's
|
|
138
|
+
`update()` is just an associative accumulation of pull counts and reward
|
|
139
|
+
sums.
|
|
140
|
+
|
|
141
|
+
The four model ids arcus routes to live in `ArcModel`, but ARC runs its
|
|
142
|
+
own model catalog independently and can rename or retire an entry at
|
|
143
|
+
any time. `src/arcus/routing/model_catalog.py` checks the configured
|
|
144
|
+
list against what ARC is actually serving (cached for a few hours so
|
|
145
|
+
this doesn't cost a network round trip on every call) and quietly drops
|
|
146
|
+
anything that's no longer live, rather than routing to a model
|
|
147
|
+
guaranteed to fail. Local history logged under a since-renamed model id
|
|
148
|
+
is skipped the same way when the bandit's state gets rebuilt.
|
|
149
|
+
|
|
150
|
+
Optionally, code, math, and long-document questions can route across
|
|
151
|
+
ARC's `-thinking-*` reasoning-effort model variants too
|
|
152
|
+
(`arcus config set enable_reasoning_variants true`, default off).
|
|
153
|
+
Everyday questions stay on the fast base four either way. This is unit
|
|
154
|
+
tested but hasn't run against a real ARC key from this environment.
|
|
155
|
+
ARC's docs list these as separate catalog ids rather than a parameter
|
|
156
|
+
on the base model, the same pattern already confirmed for web search's
|
|
157
|
+
legacy-tool-calling variants below, but that's unverified here. Ask a
|
|
158
|
+
code or math question after turning it on and confirm it actually
|
|
159
|
+
answers before trusting it.
|
|
160
|
+
|
|
161
|
+
### Quality gate
|
|
162
|
+
|
|
163
|
+
Five independent checks run over every response: truncation
|
|
164
|
+
(`finish_reason == "length"`), empty output, repetition (trigram
|
|
165
|
+
duplication ratio), refusal-phrase matching, and optional Pydantic
|
|
166
|
+
schema validation for structured-output requests. Any failure logs a
|
|
167
|
+
negative reward for that model in that context and retries with a
|
|
168
|
+
different one, up to once per available arm, before giving up and
|
|
169
|
+
returning the last attempt. See `src/arcus/quality/gate.py`.
|
|
170
|
+
|
|
171
|
+
ARC caps concurrent requests per account rather than per model, so a
|
|
172
|
+
429 doesn't mean the model that was just called is bad, switching to a
|
|
173
|
+
different arm wouldn't help either. A rate limit gets a few short
|
|
174
|
+
retries against the same model before it's treated as a real failure,
|
|
175
|
+
so one busy moment doesn't unfairly tank that model's learned reward.
|
|
176
|
+
|
|
177
|
+
Similarly, ARC's access restriction (see Install below) applies to the
|
|
178
|
+
whole account, not one model, so hitting it stops the request
|
|
179
|
+
immediately with a clear message instead of cycling through every arm
|
|
180
|
+
against the same wall, and doesn't count against any model's reward.
|
|
181
|
+
|
|
182
|
+
### Semantic cache
|
|
183
|
+
|
|
184
|
+
Local `sentence-transformers` embeddings (`all-MiniLM-L6-v2`), cosine
|
|
185
|
+
similarity lookup against everything stored so far. Two things keep it
|
|
186
|
+
from just being a naive "similar enough, ship it" cache:
|
|
187
|
+
|
|
188
|
+
- **Volatility classification** — a query containing words like
|
|
189
|
+
"today," "current," or "latest" gets a TTL of zero (never actually
|
|
190
|
+
served stale), stable conceptual questions get a week.
|
|
191
|
+
- **Parameter-diff check** — before trusting a high-similarity match,
|
|
192
|
+
numbers and capitalized entities extracted from both queries are
|
|
193
|
+
compared. "when is project 2 due" and "when is project 3 due" read as
|
|
194
|
+
almost identical to a cosine similarity score, this check catches
|
|
195
|
+
that they're different questions.
|
|
196
|
+
|
|
197
|
+
Measured against a 62-pair labeled benchmark of true paraphrases and
|
|
198
|
+
near-duplicate-but-different prompts (`src/arcus/cache/benchmark.py`):
|
|
199
|
+
|
|
200
|
+
| approach | precision | recall |
|
|
201
|
+
| -------------------------- | --------- | ------ |
|
|
202
|
+
| naive cosine similarity | 0.306 | 0.688 |
|
|
203
|
+
| + parameter-diff check | 1.000 | 0.625 |
|
|
204
|
+
|
|
205
|
+
The param-diff check trades some recall (it rejects a few pairs it
|
|
206
|
+
shouldn't, "World War 1" vs "the First World War" gets flagged as a
|
|
207
|
+
conflicting parameter, a known and documented limitation) for a real
|
|
208
|
+
jump in precision, going from roughly 1-in-3 cache hits being wrong to
|
|
209
|
+
zero false hits in this benchmark.
|
|
210
|
+
|
|
211
|
+
### Offline policy evaluation and regret benchmarking
|
|
212
|
+
|
|
213
|
+
Every request logs the propensity (the probability the routing policy
|
|
214
|
+
assigned to whichever model it picked), which makes it possible to
|
|
215
|
+
estimate how a *different* policy would have performed without ever
|
|
216
|
+
running it live, using only the log that already exists. `arcus` logs
|
|
217
|
+
propensity from the very first request, this can't be added
|
|
218
|
+
retroactively to old data.
|
|
219
|
+
|
|
220
|
+
`src/arcus/eval/offline.py` implements inverse propensity scoring (IPS)
|
|
221
|
+
and doubly robust (DR) estimators plus percentile bootstrap confidence
|
|
222
|
+
intervals, and `evaluate_policies()` produces a comparison table:
|
|
223
|
+
the logged policy's actual average reward next to estimated values for
|
|
224
|
+
any alternative policies you want to compare it against (e.g. "what if
|
|
225
|
+
we'd always used the cheapest model").
|
|
226
|
+
|
|
227
|
+
Regret benchmarking is a different technique: it needs a *known*
|
|
228
|
+
ground-truth reward per arm to measure regret against, which real
|
|
229
|
+
traffic can't provide (a real request only ever explores one model per
|
|
230
|
+
round, so there's no way to know what the other three would have
|
|
231
|
+
scored). `src/arcus/eval/regret.py` simulates each algorithm against a
|
|
232
|
+
labeled synthetic reward environment instead, this is the standard way
|
|
233
|
+
to study a bandit algorithm's exploration behavior on its own, separate
|
|
234
|
+
from real-world model quality. A sample run (2000 rounds, seed 42):
|
|
235
|
+
|
|
236
|
+
| algorithm | final cumulative regret |
|
|
237
|
+
| -------------- | ------------------------ |
|
|
238
|
+
| epsilon-greedy | 7.9 |
|
|
239
|
+
| thompson | 25.4 |
|
|
240
|
+
| ucb1 | 58.6 |
|
|
241
|
+
| random | 76.9 |
|
|
242
|
+
|
|
243
|
+
All three real algorithms land well below the random baseline, which is
|
|
244
|
+
the actual point: they're spending far less time on worse-than-best
|
|
245
|
+
arms than picking blindly would.
|
|
246
|
+
|
|
247
|
+
### Document Q&A and web search
|
|
248
|
+
|
|
249
|
+
Both build directly on capabilities ARC's own API already provides,
|
|
250
|
+
rather than reimplementing them:
|
|
251
|
+
|
|
252
|
+
- **`arcus --doc <path> "question"`** uploads the file to ARC's RAG
|
|
253
|
+
endpoint, attaches it to the request, and deletes it from your ARC
|
|
254
|
+
account again once you have an answer. Works across all four core
|
|
255
|
+
models, confirmed live against the real API.
|
|
256
|
+
- **`arcus --web "question"`** routes to ARC's `server:websearch` tool
|
|
257
|
+
through three of its "legacy-tool-calling" model variants
|
|
258
|
+
(`gpt-oss-120b`, `Kimi-K3`, and the older `glm-52` variant) confirmed
|
|
259
|
+
to actually perform a real search and cite sources. A fourth,
|
|
260
|
+
DeepSeek's legacy variant, accepts the same request without erroring
|
|
261
|
+
but doesn't reliably act on it, live testing caught it answering a
|
|
262
|
+
time-sensitive question wrong with no citation, so it's left out.
|
|
263
|
+
|
|
264
|
+
Both skip the semantic cache: a cached answer keyed on question text
|
|
265
|
+
alone would risk answering about the wrong document, or serving a
|
|
266
|
+
web-search answer that's since gone stale. See `src/arcus/cli.py`
|
|
267
|
+
(`run_doc_ask`, `run_web_ask`) and `ArcAdapter.upload_file`/
|
|
268
|
+
`delete_file` in `src/arcus/adapters/arc_adapter.py`.
|
|
269
|
+
|
|
270
|
+
## Install
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
pip install arcus-cli
|
|
274
|
+
# or, with uv
|
|
275
|
+
uv tool install arcus-cli
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Or run from source:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
git clone https://github.com/ShakirFarhan/Arcus.git
|
|
282
|
+
cd Arcus
|
|
283
|
+
uv sync
|
|
284
|
+
uv run arcus "explain how binary search works"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
First run walks you through a one-time setup: it asks for your ARC key
|
|
288
|
+
(get one from `llm.arc.vt.edu` under User profile > Settings > Account
|
|
289
|
+
> API keys), makes one live call to check it works, and saves it to
|
|
290
|
+
`~/.config/arcus/config.toml` with `chmod 600`. No separate setup
|
|
291
|
+
command to remember.
|
|
292
|
+
|
|
293
|
+
ARC restricts the API to VT's campus network, so this (and every
|
|
294
|
+
`arcus` call after it) needs either an on-campus connection or VT's
|
|
295
|
+
VPN. Arcus surfaces this as a clear message rather than the generic
|
|
296
|
+
"no usable response" error when it happens.
|
|
297
|
+
|
|
298
|
+
For tab completion on the `chat`/`stats`/`eval`/`models`/`config`/
|
|
299
|
+
`--random`/`--model`/`--image`/`--doc`/`--web` words, add one of these
|
|
300
|
+
to your shell config:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# zsh, in ~/.zshrc
|
|
304
|
+
eval "$(arcus --completion zsh)"
|
|
305
|
+
|
|
306
|
+
# bash, in ~/.bashrc
|
|
307
|
+
eval "$(arcus --completion bash)"
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Usage
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# ask something directly
|
|
314
|
+
arcus "explain how binary search works"
|
|
315
|
+
|
|
316
|
+
# pipe an error straight in
|
|
317
|
+
python broken.py 2>&1 | arcus
|
|
318
|
+
|
|
319
|
+
# or combine piped context with an explicit instruction
|
|
320
|
+
python broken.py 2>&1 | arcus "why is this failing"
|
|
321
|
+
|
|
322
|
+
# force the random-routing baseline instead of the learned bandit policy
|
|
323
|
+
arcus --random "explain how binary search works"
|
|
324
|
+
|
|
325
|
+
# skip the bandit entirely and pin a specific model for this one call
|
|
326
|
+
arcus --model GLM-5.3 "explain how binary search works"
|
|
327
|
+
|
|
328
|
+
# see how it's doing
|
|
329
|
+
arcus stats
|
|
330
|
+
|
|
331
|
+
# compare the routing policy actually run against offline alternatives
|
|
332
|
+
arcus eval
|
|
333
|
+
|
|
334
|
+
# see every model ARC is currently serving, and which ones arcus routes to
|
|
335
|
+
arcus models
|
|
336
|
+
|
|
337
|
+
# hold a multi-turn conversation instead of a single question
|
|
338
|
+
arcus chat
|
|
339
|
+
|
|
340
|
+
# inside chat, --doc/--web/--image/--model all work inline, one
|
|
341
|
+
# attachment per turn: "you: --doc paper.pdf summarize this"
|
|
342
|
+
|
|
343
|
+
# save the conversation to a file when you leave
|
|
344
|
+
arcus chat --save transcript.md
|
|
345
|
+
|
|
346
|
+
# ask about an image (routes to Kimi-K3, the one ARC model documented
|
|
347
|
+
# as vision-capable)
|
|
348
|
+
arcus --image screenshot.png "what's wrong with this code?"
|
|
349
|
+
|
|
350
|
+
# ask a question about a document, ARC handles the retrieval
|
|
351
|
+
arcus --doc syllabus.pdf "when is the midterm?"
|
|
352
|
+
|
|
353
|
+
# ask something that needs current information
|
|
354
|
+
arcus --web "what's the latest release of Python?"
|
|
355
|
+
|
|
356
|
+
# view or change local settings
|
|
357
|
+
arcus config
|
|
358
|
+
arcus config set bandit_algorithm ucb1
|
|
359
|
+
|
|
360
|
+
# check which version is installed
|
|
361
|
+
arcus --version
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Quick reference, details for each are below:
|
|
365
|
+
|
|
366
|
+
| Command | What it does |
|
|
367
|
+
| --- | --- |
|
|
368
|
+
| `arcus "<question>"` | Ask something, routed through the bandit + quality gate. |
|
|
369
|
+
| `arcus --random "<question>"` | Same, but routes randomly instead of using the learned policy. |
|
|
370
|
+
| `arcus --model NAME "<question>"` | Skip routing, pin one specific model. |
|
|
371
|
+
| `arcus --image PATH "<question>"` | Ask about an image (vision-capable model only). |
|
|
372
|
+
| `arcus --doc PATH "<question>"` | Ask about an uploaded document (RAG). |
|
|
373
|
+
| `arcus --web "<question>"` | Ask something needing current information (web search). |
|
|
374
|
+
| `arcus chat [--save PATH]` | Multi-turn conversation; `--doc`/`--web`/`--image`/`--model` all work inline per turn. |
|
|
375
|
+
| `arcus stats` | Local routing performance so far. |
|
|
376
|
+
| `arcus eval` | Offline comparison of the routing policy against alternatives. |
|
|
377
|
+
| `arcus models` | ARC's live model catalog vs. what arcus routes to. |
|
|
378
|
+
| `arcus config [set ...]` | View or change local settings. |
|
|
379
|
+
| `arcus --version` | Installed version. |
|
|
380
|
+
|
|
381
|
+
`arcus chat` opens a REPL that remembers everything said earlier in that
|
|
382
|
+
session (resending the growing transcript each turn, since ARC's API has
|
|
383
|
+
no session concept of its own) and routes each turn through the same
|
|
384
|
+
bandit/quality-gate/logging pipeline as a one-shot `arcus "..."` call.
|
|
385
|
+
Type `exit` or press ctrl-d to leave. The conversation only lives for
|
|
386
|
+
that one run, closing the terminal loses it, unless you pass `--save
|
|
387
|
+
<path>`, which writes the full transcript (not just whatever's still in
|
|
388
|
+
the trimmed context window) to a markdown file when you exit.
|
|
389
|
+
|
|
390
|
+
`--doc PATH`, `--web`, `--image PATH`, and `--model NAME` all work
|
|
391
|
+
inline inside `arcus chat` too, typed as part of a turn (`you: --doc
|
|
392
|
+
paper.pdf summarize this`), one attachment per turn, the same rules as
|
|
393
|
+
below apply. The attachment only applies to that one turn, a later turn
|
|
394
|
+
that wants to keep asking about the same document attaches it again.
|
|
395
|
+
|
|
396
|
+
`arcus --image <path> "question"` attaches an image to a one-shot
|
|
397
|
+
question. It always goes to Kimi-K3 rather than through the usual
|
|
398
|
+
bandit comparison, confirmed directly against the API to be the only
|
|
399
|
+
one of the four models that can actually see an image, GLM-5.3 and
|
|
400
|
+
DeepSeek-V4-Flash both reject image content outright and gpt-oss-120b
|
|
401
|
+
accepts the request but reports it can't see anything. Skips the
|
|
402
|
+
semantic cache entirely too, matching on the question text alone would
|
|
403
|
+
risk serving back an answer about a completely different image.
|
|
404
|
+
|
|
405
|
+
`arcus --doc <path> "question"` and `arcus --web "question"` work the
|
|
406
|
+
same way as `--image`, cache skipped, see "Document Q&A and web search"
|
|
407
|
+
above for what each actually does. Only one of `--image`, `--doc`, or
|
|
408
|
+
`--web` can be used at a time.
|
|
409
|
+
|
|
410
|
+
`arcus --model NAME "question"` skips the bandit entirely and always
|
|
411
|
+
uses that model, checked against ARC's live catalog first. Since
|
|
412
|
+
there's only one arm, the quality gate's checks (empty, truncated,
|
|
413
|
+
repetitive, refusal) still run and still get reported, there's just no
|
|
414
|
+
other model left to fall back to if it fails, that's the point of an
|
|
415
|
+
explicit override. Combine with `--web` or `--image` and the name has
|
|
416
|
+
to be one of the models valid for that mode.
|
|
417
|
+
|
|
418
|
+
`arcus config` shows your current settings (the API key masked) and the
|
|
419
|
+
path to the config file. `arcus config set bandit_algorithm <algo>`
|
|
420
|
+
changes which bandit algorithm arcus uses without hand-editing the TOML
|
|
421
|
+
file. `arcus config set enable_reasoning_variants <true|false>` turns
|
|
422
|
+
the reasoning-effort routing described above on or off. Re-keying isn't
|
|
423
|
+
supported here on purpose, delete the config file and run `arcus` again
|
|
424
|
+
to go through setup fresh.
|
|
425
|
+
|
|
426
|
+
`arcus stats` reads your local SQLite log and prints a `rich`-formatted
|
|
427
|
+
table: request count, average reward, average latency, and cost score
|
|
428
|
+
per model per mode, plus your cache hit rate and how many attempts the
|
|
429
|
+
quality gate has caught and retried. Entirely local, no network call.
|
|
430
|
+
|
|
431
|
+
`arcus eval` runs the offline policy evaluation described above against
|
|
432
|
+
your own logged history and prints the comparison table (IPS and
|
|
433
|
+
doubly-robust estimates with 95% confidence intervals for the greedy
|
|
434
|
+
policy and each "always use model X" baseline, against what actually
|
|
435
|
+
ran). Below 30 logged bandit-mode requests it still prints the table but
|
|
436
|
+
flags the numbers as illustrative only, a bootstrap confidence interval
|
|
437
|
+
on a handful of rows isn't a reliable comparison yet.
|
|
438
|
+
|
|
439
|
+
## Status
|
|
440
|
+
|
|
441
|
+
Everything above is built and working, adapter, context classification,
|
|
442
|
+
all three bandit algorithms, the reward function, the quality gate, the
|
|
443
|
+
semantic cache, the offline eval / regret code. The CLI covers all of
|
|
444
|
+
it: asking directly (with an optional `--model` override), chat with
|
|
445
|
+
inline attachments and transcript export, image/doc/web modes, config,
|
|
446
|
+
stats, and eval.
|
|
447
|
+
|
|
448
|
+
Live-tested against a real ARC key: all four models answer correctly
|
|
449
|
+
(`tests/adapters/test_arc_adapter_live.py`), and a full `arcus "..."`
|
|
450
|
+
run has gone through the real pipeline end to end, classification,
|
|
451
|
+
cache miss, routing, an actual ARC call, the quality gate, logging,
|
|
452
|
+
caching. Image input, document Q&A, and web search have each gotten a
|
|
453
|
+
real run too. Test suite: 273 passing with a key set (269 + 4
|
|
454
|
+
live-only), 4 skipped without one.
|
|
455
|
+
|
|
456
|
+
Exception: reasoning-effort variant routing (`enable_reasoning_variants`)
|
|
457
|
+
has only run against a fake adapter so far, which is why it defaults
|
|
458
|
+
off. See "Adaptive routing" above.
|
|
459
|
+
|
|
460
|
+
ARC's models are reasoning models under the hood, they write to a
|
|
461
|
+
hidden `reasoning` field before `content`, so a tight `max_tokens`
|
|
462
|
+
budget can get eaten up before any real answer shows up. The CLI never
|
|
463
|
+
sets `max_tokens` itself, so this doesn't affect normal usage, it only
|
|
464
|
+
matters if you're calling the adapter directly with your own tight
|
|
465
|
+
budget.
|
|
466
|
+
|
|
467
|
+
Still open:
|
|
468
|
+
|
|
469
|
+
- Real logged usage is thin (a handful of manual runs). `arcus eval`
|
|
470
|
+
runs today, it just doesn't have enough data yet, and says so
|
|
471
|
+
instead of faking confidence.
|
|
472
|
+
- Reasoning-effort routing needs a live-key run before it's safe to
|
|
473
|
+
default on.
|
|
474
|
+
|
|
475
|
+
## Security & privacy
|
|
476
|
+
|
|
477
|
+
- Each install uses its own user's ARC key. Keys are never shared,
|
|
478
|
+
bundled, or sent anywhere but ARC's own endpoint.
|
|
479
|
+
- No data leaves your machine except to ARC itself, with your own key.
|
|
480
|
+
Request logs, cache entries, and stats are all local SQLite, nothing
|
|
481
|
+
is aggregated or reported anywhere else.
|
|
482
|
+
- `arcus --doc` uploads the whole file to your ARC account temporarily
|
|
483
|
+
(deleted again once you have an answer), and `arcus --web` sends your
|
|
484
|
+
question through ARC's own web search tool. Both stay within ARC,
|
|
485
|
+
same as every other request, but a document leaving your machine
|
|
486
|
+
entirely (even briefly, even to your own account) is worth knowing
|
|
487
|
+
about explicitly.
|
|
488
|
+
- This tool hasn't been through ARC's security review for regulated
|
|
489
|
+
data (FERPA records, health data, etc.) the way ARC's own web
|
|
490
|
+
interface has. Don't route sensitive regulated data through it.
|
|
491
|
+
- MIT licensed, source is fully readable, that's the actual trust
|
|
492
|
+
mechanism here rather than a policy document.
|
|
493
|
+
|
|
494
|
+
## License
|
|
495
|
+
|
|
496
|
+
MIT, see `LICENSE`.
|