Repository navigation
(MOT-4412) feat(llm-router): make the config form operator-friendly #789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { minutesToMs, msToMinutes } from './duration' | ||
|
|
||
| describe('timeout minutes', () => { | ||
| it('converts the router defaults without rounding error', () => { | ||
| expect(msToMinutes(300_000)).toBe(5) | ||
| expect(msToMinutes(120_000)).toBe(2) | ||
| expect(minutesToMs(5)).toBe(300_000) | ||
| expect(minutesToMs(2)).toBe(120_000) | ||
| }) | ||
|
|
||
| it('round-trips fractional minutes to whole milliseconds', () => { | ||
| expect(minutesToMs(1.5)).toBe(90_000) | ||
| expect(msToMinutes(90_000)).toBe(1.5) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /** Stream/idle timeouts are stored as milliseconds; the form edits minutes. */ | ||
|
|
||
| export function msToMinutes(ms: number): number { | ||
| return ms / 60_000 | ||
| } | ||
|
|
||
| export function minutesToMs(minutes: number): number { | ||
| return Math.round(minutes * 60_000) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export { errorAt, pointer } from './pointers' | ||
|
|
||
| export function FieldError({ message }: { message: string | null }) { | ||
| if (!message) return null | ||
| return ( | ||
| <div className="llmr-cfg-field-error" role="alert"> | ||
| {message} | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { moveItem, winningHeuristicIndex } from './heuristics' | ||
|
|
||
| describe('winningHeuristicIndex', () => { | ||
| const rows = [ | ||
| { pattern: '^gpt-', provider: 'openai' }, | ||
| { pattern: 'claude', provider: 'anthropic' }, | ||
| { pattern: '(', provider: 'broken' }, | ||
| { pattern: '', provider: 'openai' }, | ||
| ] | ||
|
|
||
| it('returns the first regex that matches the model id', () => { | ||
| expect(winningHeuristicIndex('gpt-4.1', rows)).toBe(0) | ||
| expect(winningHeuristicIndex('claude-sonnet-4', rows)).toBe(1) | ||
| }) | ||
|
|
||
| it('skips invalid regex and empty patterns', () => { | ||
| expect(winningHeuristicIndex('nope', rows)).toBe(null) | ||
| }) | ||
|
|
||
| it('returns null when the model id is empty', () => { | ||
| expect(winningHeuristicIndex('', rows)).toBe(null) | ||
| expect(winningHeuristicIndex(' ', rows)).toBe(null) | ||
| }) | ||
| }) | ||
|
|
||
| describe('moveItem', () => { | ||
| it('moves a row up or down and no-ops out of range', () => { | ||
| expect(moveItem(['a', 'b', 'c'], 2, 0)).toEqual(['c', 'a', 'b']) | ||
| expect(moveItem(['a', 'b', 'c'], 0, 1)).toEqual(['b', 'a', 'c']) | ||
| expect(moveItem(['a', 'b', 'c'], 0, -1)).toEqual(['a', 'b', 'c']) | ||
| expect(moveItem(['a', 'b', 'c'], 0, 3)).toEqual(['a', 'b', 'c']) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /** Draft-only heuristic preview and row reorder. Order is routing priority. */ | ||
|
|
||
| export function winningHeuristicIndex(model: string, rows: { pattern: string; provider: string }[]): number | null { | ||
| const needle = model.trim() | ||
| if (!needle) return null | ||
| for (let i = 0; i < rows.length; i++) { | ||
| const pattern = rows[i].pattern | ||
| if (!pattern || !rows[i].provider) continue | ||
| try { | ||
| if (new RegExp(pattern).test(needle)) return i | ||
| } catch { | ||
| // An invalid operator regex never takes the router down. | ||
| } | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| export function moveItem<T>(items: T[], from: number, to: number): T[] { | ||
| if (to < 0 || to >= items.length || from === to) return items | ||
| const next = [...items] | ||
| const [row] = next.splice(from, 1) | ||
| next.splice(to, 0, row) | ||
|
Comment on lines
+18
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 4 'moveItem\s*\(|from: number|to: number' llm-router/ui/srcRepository: iii-hq/workers Length of output: 3373 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- heuristics.ts ---'
cat -n llm-router/ui/src/configuration/heuristics.ts
printf '%s\n' '--- heuristics.test.ts ---'
cat -n llm-router/ui/src/configuration/heuristics.test.ts
printf '%s\n' '--- runtime splice probes ---'
node - <<'JS'
const cases = [
['negative from', ['a', 'b', 'c'], -1, 0],
['from >= length', ['a', 'b', 'c'], 3, 1],
['fractional from', ['a', 'b', 'c'], 1.5, 0],
['NaN from', ['a', 'b', 'c'], NaN, 1],
['negative fractional from', ['a', 'b', 'c'], -1.5, 0],
]
for (const [name, items, from, to] of cases) {
if (to < 0 || to >= items.length || from === to) {
console.log(name, JSON.stringify(items))
continue
}
const next = [...items]
const [row] = next.splice(from, 1)
next.splice(to, 0, row)
console.log(name, JSON.stringify(next))
}
JSRepository: iii-hq/workers Length of output: 2808 Validate the source index before calling Invalid 🤖 Prompt for AI Agents |
||
| return next | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: iii-hq/workers
Length of output: 16524
🏁 Script executed:
Repository: iii-hq/workers
Length of output: 36939
🏁 Script executed:
Repository: iii-hq/workers
Length of output: 232
Prevent catastrophic regex evaluation on the browser main thread.
winningHeuristicIndexsynchronously evaluates patterns from the draft configuration against free-form probe input. A pattern such as^(a+)+$can block the UI for seconds. Thecatchblock handles syntax errors only. Use a linear-time regex engine or a terminable worker with a strict timeout. Do not rely on input-length limits alone.🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 9-9: Do not use variable for regular expressions
Context: new RegExp(pattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity. Security best practice.
(regexp-non-literal-typescript)
[warning] 9-9: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(pattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity
(regexp-from-variable)
🤖 Prompt for AI Agents
Source: Linters/SAST tools