omniharness-cli 0.1.84 → 0.1.85

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.
@@ -145,17 +145,27 @@ export class OmniRouteClient {
145
145
  }
146
146
  /** List every model id the gateway exposes, including `auto/*` virtual combos and individual providers. */
147
147
  async listModels(signal) {
148
+ return (await this.listCatalog(signal)).map((entry) => entry.id);
149
+ }
150
+ /**
151
+ * The catalog with the context window each entry advertises. OmniRoute
152
+ * states `context_length` on providers' models, on `auto/*` engines and on
153
+ * combos alike; `max_input_tokens` stands in when an entry carries only that.
154
+ */
155
+ async listCatalog(signal) {
148
156
  const response = await this.requestWithRetry('/v1/models', { method: 'GET', signal });
149
157
  const payload = await response.json();
150
158
  const data = this.isRecord(payload) && Array.isArray(payload.data) ? payload.data : [];
151
- const ids = [];
159
+ const entries = [];
152
160
  for (const entry of data) {
153
- if (this.isRecord(entry) && typeof entry.id === 'string' && entry.id.trim() !== '')
154
- ids.push(entry.id);
161
+ if (!this.isRecord(entry) || typeof entry.id !== 'string' || entry.id.trim() === '')
162
+ continue;
163
+ const window = this.positive(entry.context_length) ?? this.positive(entry.max_input_tokens);
164
+ entries.push(window !== undefined ? { id: entry.id, contextLength: window } : { id: entry.id });
155
165
  }
156
- if (ids.length === 0)
166
+ if (entries.length === 0)
157
167
  throw new OmniRouteError(response.status, 'invalid models response');
158
- return ids;
168
+ return entries;
159
169
  }
160
170
  /** Retry transient responses for idempotent metadata reads without touching chat/tool requests. */
161
171
  async requestWithRetry(path, init) {
@@ -431,6 +441,9 @@ export class OmniRouteClient {
431
441
  const provider = headers.get('x-omniroute-provider') ?? decision.provider;
432
442
  if (provider)
433
443
  fallback.activeProvider = provider;
444
+ const model = headers.get('x-omniroute-model');
445
+ if (model && model.trim() !== '')
446
+ fallback.model = model.trim();
434
447
  if (decision.strategy)
435
448
  fallback.strategy = decision.strategy;
436
449
  if (decision.latencyMs !== undefined)
@@ -485,6 +498,9 @@ export class OmniRouteClient {
485
498
  number(value) {
486
499
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
487
500
  }
501
+ positive(value) {
502
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
503
+ }
488
504
  safeParse(text) {
489
505
  try {
490
506
  return JSON.parse(text);
@@ -6,6 +6,12 @@
6
6
  * a model id (or the provider name OmniRoute reports) to a token budget;
7
7
  * `contextMeter` turns "tokens in" into a fill fraction and a zone the UI
8
8
  * colours (ok / warn / danger) using the playbook's 70 / 90 thresholds.
9
+ *
10
+ * The gateway's catalog is the first source: `/v1/models` states a
11
+ * `context_length` per entry, and `windowIndex` turns that into a lookup the
12
+ * meter consults before the substring table below. The table remains the
13
+ * answer for a model the catalog does not size, and when the catalog could
14
+ * not be read at all.
9
15
  */
10
16
  /** Known windows keyed by a substring of the model / provider id (longest match wins). */
11
17
  const WINDOWS = [
@@ -35,12 +41,52 @@ const WINDOWS = [
35
41
  ];
36
42
  /** Fallback window when nothing matches — conservative so the meter warns early rather than late. */
37
43
  export const DEFAULT_WINDOW = 128_000;
44
+ /**
45
+ * Build the lookup from a catalog. Each entry is keyed by its full id and,
46
+ * when the id carries a provider prefix, by the bare model name after it —
47
+ * `X-OmniRoute-Model` reports the upstream name (`claude-sonnet-4-6`) while
48
+ * the catalog lists it under a prefix (`cc/claude-sonnet-4-6`). The first
49
+ * entry to claim a bare name keeps it, so a `dual`-mode mirror never
50
+ * contradicts its primary.
51
+ */
52
+ export function windowIndex(entries) {
53
+ const index = new Map();
54
+ for (const entry of entries) {
55
+ const tokens = entry.contextLength;
56
+ if (tokens === undefined || !Number.isFinite(tokens) || tokens <= 0)
57
+ continue;
58
+ const id = entry.id.trim().toLowerCase();
59
+ if (id === '')
60
+ continue;
61
+ if (!index.has(id))
62
+ index.set(id, tokens);
63
+ const slash = id.indexOf('/');
64
+ if (slash > 0 && slash < id.length - 1) {
65
+ const bare = id.slice(slash + 1);
66
+ if (!index.has(bare))
67
+ index.set(bare, tokens);
68
+ }
69
+ }
70
+ return index;
71
+ }
38
72
  /**
39
73
  * Resolve a context window for a model id and/or the provider OmniRoute
40
- * reported for the turn. Matching is case-insensitive substring; the longest
41
- * matching pattern wins so `gpt-4o-mini` beats `gpt-4o`.
74
+ * reported for the turn. A catalog `known` answers first, by the exact id and
75
+ * then by the bare name after a provider prefix. Otherwise matching is
76
+ * case-insensitive substring; the longest matching pattern wins so
77
+ * `gpt-4o-mini` beats `gpt-4o`.
42
78
  */
43
- export function windowFor(modelId, provider) {
79
+ export function windowFor(modelId, provider, known) {
80
+ if (known && modelId) {
81
+ const id = modelId.trim().toLowerCase();
82
+ const exact = known.get(id);
83
+ if (exact !== undefined)
84
+ return exact;
85
+ const slash = id.indexOf('/');
86
+ const bare = slash > 0 ? known.get(id.slice(slash + 1)) : undefined;
87
+ if (bare !== undefined)
88
+ return bare;
89
+ }
44
90
  const haystack = `${modelId ?? ''} ${provider ?? ''}`.toLowerCase();
45
91
  let best;
46
92
  let bestLen = 0;
@@ -53,8 +99,8 @@ export function windowFor(modelId, provider) {
53
99
  return best ?? DEFAULT_WINDOW;
54
100
  }
55
101
  /** Build the meter. `used` below 0 clamps to 0; the fraction is capped at 1. */
56
- export function contextMeter(used, modelId, provider) {
57
- const window = windowFor(modelId, provider);
102
+ export function contextMeter(used, modelId, provider, known) {
103
+ const window = windowFor(modelId, provider, known);
58
104
  const safeUsed = Math.max(0, used);
59
105
  const fraction = Math.min(1, safeUsed / window);
60
106
  const zone = fraction >= 0.9 ? 'danger' : fraction >= 0.7 ? 'warn' : 'ok';
@@ -12,7 +12,7 @@ import { capabilityLine, recentRows, shortenPath, twoColumn } from './home.js';
12
12
  import { conversationWidth, overflowCount, sidebarMode, SIDEBAR_WIDTH, todoRows, usageRows, clip as clipRow } from './sidebar.js';
13
13
  import { planViewport } from './viewport.js';
14
14
  import { statusMarker, toolHead } from './toolrow.js';
15
- import { contextMeter, meterBar } from './modelWindows.js';
15
+ import { contextMeter, meterBar, windowIndex } from './modelWindows.js';
16
16
  import { BEL, SYNC_QUERY, isSyncOutputReply, osc9Notify, osc52Copy, shouldNudgeOnFinish, wrapSynchronizedOutput } from './termcaps.js';
17
17
  import { KITTY_POP, KITTY_PUSH, KITTY_QUERY, isEncodedKey, isKittyQueryResponse, parseRawKey } from './keys.js';
18
18
  import { ownVersion } from '../update.js';
@@ -271,6 +271,9 @@ export function TerminalInterface({ engine }) {
271
271
  const [pickerItems, setPickerItems] = useState([]);
272
272
  const [pickerIndex, setPickerIndex] = useState(0);
273
273
  const [pickerError, setPickerError] = useState();
274
+ // Context windows the catalog states, keyed by model id. Empty until the
275
+ // catalog has been read; the meter falls back to its own table meanwhile.
276
+ const [windows, setWindows] = useState(() => new Map());
274
277
  const [mode, setMode] = useState(engine.state.mode);
275
278
  const [permMode, setPermMode] = useState(engine.state.permissionMode ?? 'ask');
276
279
  const [approval, setApproval] = useState(null);
@@ -310,6 +313,10 @@ export function TerminalInterface({ engine }) {
310
313
  if (alive)
311
314
  setRecentSessions(found);
312
315
  }).catch(() => { });
316
+ void Promise.resolve().then(() => engine.client.listCatalog()).then((catalog) => {
317
+ if (alive)
318
+ setWindows(windowIndex(catalog));
319
+ }).catch(() => { });
313
320
  return () => { alive = false; };
314
321
  }, []);
315
322
  useEffect(() => {
@@ -434,7 +441,9 @@ export function TerminalInterface({ engine }) {
434
441
  const loadPicker = async () => {
435
442
  setPickerError(undefined);
436
443
  try {
437
- const [accountCombos, modelIds] = await Promise.all([engine.client.listCombos(), engine.client.listModels()]);
444
+ const [accountCombos, catalog] = await Promise.all([engine.client.listCombos(), engine.client.listCatalog()]);
445
+ setWindows(windowIndex(catalog));
446
+ const modelIds = catalog.map((entry) => entry.id);
438
447
  const items = [];
439
448
  for (const combo of accountCombos) {
440
449
  if (combo.name.trim() !== '' && !items.some((item) => item.id === combo.name)) {
@@ -984,7 +993,9 @@ export function TerminalInterface({ engine }) {
984
993
  // The prompt tokens of the last completion, as the gateway counted them,
985
994
  // are the size of the context the next turn will carry.
986
995
  const contextTokens = metrics.usage?.contextTokens ?? 0;
987
- const meter = contextMeter(contextTokens, engine.state.activeModel, metrics.fallback.activeProvider);
996
+ // Sized to the model that answered, when the gateway named one: an `auto/*`
997
+ // engine or a combo can land anywhere, and the window is that model's.
998
+ const meter = contextMeter(contextTokens, metrics.fallback.model ?? engine.state.activeModel, metrics.fallback.activeProvider, windows);
988
999
  const meterColor = meter.zone === 'danger' ? PALETTE.error : meter.zone === 'warn' ? PALETTE.warn : PALETTE.muted;
989
1000
  const contextLabel = contextTokens > 0 ? `ctx ${meterBar(meter.fraction, 8)} ${Math.round(meter.fraction * 100)}%` : '';
990
1001
  const compression = metrics.compression.inputTokens > 0 ? `${Math.round((1 - metrics.compression.ratio) * 100)}% ${metrics.compression.strategy.toUpperCase()}` : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniharness-cli",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "description": "OmniHarness — local-first agent orchestration harness for OmniRoute.",
5
5
  "license": "MIT",
6
6
  "type": "module",