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.
Files changed (58) hide show
  1. arcus_cli-0.2.1/.github/ISSUE_TEMPLATE/bug_report.yml +47 -0
  2. arcus_cli-0.2.1/.github/ISSUE_TEMPLATE/feature_request.yml +25 -0
  3. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/.github/workflows/release.yml +13 -0
  4. arcus_cli-0.2.1/CONTRIBUTING.md +48 -0
  5. arcus_cli-0.2.1/PKG-INFO +496 -0
  6. arcus_cli-0.2.1/README.md +466 -0
  7. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/pyproject.toml +1 -1
  8. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/adapters/arc_adapter.py +45 -2
  9. arcus_cli-0.2.1/src/arcus/cli.py +1078 -0
  10. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/config.py +19 -4
  11. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/quality/gate.py +63 -9
  12. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/bandit.py +22 -2
  13. arcus_cli-0.2.1/src/arcus/routing/model_catalog.py +96 -0
  14. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/reward.py +9 -13
  15. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/warm_start.py +9 -6
  16. arcus_cli-0.2.1/tests/adapters/test_arc_adapter.py +167 -0
  17. arcus_cli-0.2.1/tests/quality/test_gate.py +402 -0
  18. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/routing/test_bandit.py +13 -2
  19. arcus_cli-0.2.1/tests/routing/test_model_catalog.py +116 -0
  20. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/routing/test_reward.py +10 -15
  21. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/routing/test_warm_start.py +31 -1
  22. arcus_cli-0.2.1/tests/test_cli.py +1846 -0
  23. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/test_config.py +30 -0
  24. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/uv.lock +1 -1
  25. arcus_cli-0.1.0/PKG-INFO +0 -308
  26. arcus_cli-0.1.0/README.md +0 -278
  27. arcus_cli-0.1.0/src/arcus/cli.py +0 -335
  28. arcus_cli-0.1.0/tests/adapters/test_arc_adapter.py +0 -70
  29. arcus_cli-0.1.0/tests/quality/test_gate.py +0 -230
  30. arcus_cli-0.1.0/tests/test_cli.py +0 -472
  31. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/.github/workflows/ci.yml +0 -0
  32. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/.gitignore +0 -0
  33. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/.python-version +0 -0
  34. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/LICENSE +0 -0
  35. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/__init__.py +0 -0
  36. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/adapters/__init__.py +0 -0
  37. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/cache/__init__.py +0 -0
  38. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/cache/benchmark.py +0 -0
  39. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/cache/semantic_cache.py +0 -0
  40. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/embeddings.py +0 -0
  41. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/eval/__init__.py +0 -0
  42. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/eval/offline.py +0 -0
  43. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/eval/regret.py +0 -0
  44. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/quality/__init__.py +0 -0
  45. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/__init__.py +0 -0
  46. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/routing/context.py +0 -0
  47. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/storage/__init__.py +0 -0
  48. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/storage/db.py +0 -0
  49. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/src/arcus/storage/stats.py +0 -0
  50. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/adapters/test_arc_adapter_live.py +0 -0
  51. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/cache/test_benchmark.py +0 -0
  52. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/cache/test_semantic_cache.py +0 -0
  53. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/eval/test_offline.py +0 -0
  54. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/eval/test_regret.py +0 -0
  55. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/routing/test_context.py +0 -0
  56. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/storage/test_db.py +0 -0
  57. {arcus_cli-0.1.0 → arcus_cli-0.2.1}/tests/storage/test_stats.py +0 -0
  58. {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.
@@ -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
+ [![CI](https://github.com/ShakirFarhan/Arcus/actions/workflows/ci.yml/badge.svg)](https://github.com/ShakirFarhan/Arcus/actions/workflows/ci.yml)
34
+ [![PyPI](https://img.shields.io/pypi/v/arcus-cli.svg)](https://pypi.org/project/arcus-cli/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/arcus-cli.svg)](https://pypi.org/project/arcus-cli/)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](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`.