Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/scripts/webui_browser_suites.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"share.spec.ts",
"session-chat-reliability.real.spec.ts",
"session-delivery-feedback.spec.ts",
"router-provider-save.spec.ts",
)
GENERAL_SHARDS = tuple(f"general-{index}" for index in range(1, 5))
VIRTUALIZATION_SPECS = (
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/windows_test_assignments.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
"tests/test_provider_tool_stream_accumulator.py",
"tests/test_provider_trace_recorder.py",
"tests/test_redaction.py",
"tests/test_router_explicit_provider_save.py",
"tests/test_safety/test_run_sandboxed_proc_cleanup.py",
"tests/test_sandbox/test_approval_runtime.py",
"tests/test_sandbox/test_authority_paths.py",
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/windows_test_durations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,7 @@
"tests/test_release_oss_mirror_workflow.py": 7.527,
"tests/test_request_proof_levers.py": 0.147,
"tests/test_root_start_scripts.py": 0.01,
"tests/test_router_explicit_provider_save.py": 0.01,
"tests/test_router_tier_contract.py": 0.423,
"tests/test_runtime_packs/test_catalog.py": 0.089,
"tests/test_runtime_packs/test_runtime_pack_manager.py": 6.264,
Expand Down
4 changes: 2 additions & 2 deletions contracts/gateway/v4/compatibility-manifest.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@
"lifecycle": "stable",
"name": "onboarding.router.configure",
"schema": "platform/onboarding-router-configure.schema.json",
"schemaSha256": "fc1e17f9043e0ec5d86d8456f4dbee224ac93c80ecfef0f89f2bc8d07900797b"
"schemaSha256": "4245ff6e0ff14f92bda59a185a571dd6a4ca5c20d45ebf18d97f67ea32653cee"
},
{
"lifecycle": "stable",
Expand Down Expand Up @@ -1455,7 +1455,7 @@
"generatorSha256": "829c5afc1ad4481e06e232cd30f7746db621efafcc9c29f6dcb01e7eb3b79cad",
"methodCount": 213,
"schemaCount": 223,
"schemaTreeSha256": "b3a598c652acefcf7ce67b5e706ca87f3b9901c64426e9dea3a79a1a3abdd856",
"schemaTreeSha256": "80c689d229c795f27e43c797468cd96a83eba981383b7e5e4bf4d7a4c9d0689c",
"toolchains": {
"legacyTypes": {
"python": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,9 @@
},
"crossProviderTiers": {
"type": [
"object",
"boolean",
"null"
],
"additionalProperties": true
]
},
"tierProviderMismatch": {
"type": [
Expand Down
26 changes: 13 additions & 13 deletions desktop/electron/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"devDependencies": {
"@types/node": "^26.6.4",
"@typescript/typescript6": "6.0.2",
"electron": "^42.11.12",
"electron": "^42.11.16",
"electron-builder": "^26.15.3",
"gsap": "^3.15.0",
"lottie-web": "^5.13.0",
Expand Down
11 changes: 11 additions & 0 deletions docs/features/squilla-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ The reset button replaces the current ladder with that provider's recommendation
including saved or unsaved custom tiers. Other primary providers do not show this
shortcut. Restoring tiers keeps the current usage mode.

Saving explicit provider and model selections for executable text tiers enables
cross-provider routing automatically. Each selected provider uses its own configured
profile; no additional settings switch is needed. This also applies when saving an
existing mixed tier table after an upgrade. An explicit `crossProviderTiers: false`
API option remains authoritative.

Loading saved settings, switching the primary provider, or re-enabling routing
without submitting tier selections does not enable cross-provider execution.
Existing primary-switch and legacy-router conflict checks still apply. Dormant
single-model settings under a static C3 fusion plan do not enable it either.

C3 can optionally use the shared multi-model fusion plan configured under
`llm_ensemble`. Enabling fusion on C3 makes it use that plan instead of its
single-model route. If the plan falls back to a single model, it uses the global
Expand Down
158 changes: 158 additions & 0 deletions opensquilla-webui/e2e/router-provider-save.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { expect, test, type Page } from '@playwright/test'
import { helloOkResponse } from './support/gateway-fixture'

const primaryModel = 'synthetic-primary-model'
const foreignModel = 'synthetic/selected-model'
type Tier = { provider: string; model: string; ensemble_enabled?: boolean }

// Exercise the real Settings UI and wire decoder against an offline Gateway.
// Backend persistence and HTTP endpoint/key selection are covered separately
// by test_router_explicit_provider_save.py.
async function installGateway(page: Page, flag: boolean | undefined, legacy: boolean) {
const router = {
enabled: true, rollout_phase: 'full', preset_binding: 'custom',
default_tier: 'c1', visual_mode: 'real_candidates',
...(flag === undefined ? {} : { cross_provider_tiers: flag }),
tiers: Object.fromEntries(['c0', 'c1', 'c2', 'c3'].map(name => [name, {
provider: legacy && name === 'c3' ? 'openrouter' : 'tokenrhythm',
model: legacy && name === 'c3' ? 'synthetic/old-model' : primaryModel,
ensemble_enabled: false,
}])) as Record<string, Tier>,
}
const config = {
llm: { provider: 'tokenrhythm', model: primaryModel },
llm_profiles: { openrouter: { model: foreignModel } },
squilla_router: router,
llm_ensemble: { enabled: false, selection_mode: 'custom_b5', candidates: [] },
memory: {}, models: {}, permissions: {}, skills: {},
}
const writes: Record<string, unknown>[] = []
let rejectNextSave = false
const methods = [
'config.get', 'config.effective', 'onboarding.catalog', 'onboarding.status',
'onboarding.models.discover', 'onboarding.llmProfile.models.discover',
'onboarding.router.configure', 'models.list', 'models.routing.get',
'providers.status', 'sessions.list', 'agents.list', 'commands.list_for_surface',
]
await page.addInitScript(() => {
localStorage.setItem('opensquilla-locale', 'en')
localStorage.setItem('opensquilla-theme', 'light')
})
await page.route('**/api/**', route => route.fulfill({ json: {} }))
await page.routeWebSocket(/\/ws$/, ws => {
ws.send(JSON.stringify({ type: 'event', event: 'connect.challenge', payload: {} }))
ws.onMessage(message => {
const frame = JSON.parse(String(message))
if (frame.type !== 'req') return
const method = frame.method as string
const respond = (payload: unknown, ok = true) => ws.send(JSON.stringify({
type: 'res', id: frame.id, ok, ...(ok ? { payload } : { error: payload }),
}))
if (method === 'connect') {
ws.send(helloOkResponse({ features: { methods, events: [] } }))
return
}
if (method === 'onboarding.router.configure') {
writes.push(frame.params)
if (rejectNextSave) {
rejectNextSave = false
respond({ code: 'INTERNAL_ERROR', message: 'Synthetic save failure' }, false)
return
}
if (frame.params.crossProviderTiers !== true) {
respond({ code: 'ROUTER_PROVIDER_CONFLICT', message: 'Cross-provider execution was not selected' }, false)
return
}
router.cross_provider_tiers = true
router.tiers = structuredClone(frame.params.tiers)
respond({ changed: true, restartRequired: false, configPath: '/synthetic/config.toml', entry: router, warnings: [] })
return
}
if (method.includes('discover')) {
const foreign = frame.params?.providerId === 'openrouter'
respond({ ok: true, source: 'live', models: (foreign ? [foreignModel, 'synthetic/old-model'] : [primaryModel])
.map(id => ({ id, name: id, capabilities: ['chat'] })) })
return
}
const payloads: Record<string, unknown> = {
'config.get': config,
'config.effective': { fields: { 'llm.provider': { value: 'tokenrhythm', source: 'config' } } },
'onboarding.catalog': { providers: ['tokenrhythm', 'openrouter'].map(providerId => ({
providerId, label: providerId === 'tokenrhythm' ? 'TokenRhythm' : 'OpenRouter',
runtimeSupported: true, requiresApiKey: true, routerSupported: true,
selectableModelCatalog: 'verified_live', fields: [{ name: 'model', label: 'Model', required: true }],
})) },
'onboarding.status': {
hasConfig: true, llmConfigured: true,
llmCredentialStatus: { provider: 'tokenrhythm', available: true, source: 'explicit' },
llmProfileStatus: [{ provider: 'openrouter', ready: true, credentialSource: 'profile', primaryEligible: true }],
sectionDetails: { router: { enabled: true, routerBinding: 'custom' } },
},
'models.routing.get': { mode: 'router' }, 'models.list': { models: [], errors: [] },
'agents.list': { agents: [] }, 'commands.list_for_surface': { commands: [] },
'sessions.list': { sessions: [], count: 0, ts: 1800000000, has_more: false },
}
respond(payloads[method] ?? {})
})
})
return { router, writes, rejectNext: () => { rejectNextSave = true } }
}

async function selectForeignModel(page: Page, legacy: boolean) {
if (!legacy) {
await page.getByRole('combobox', { name: 'c3 request entry', exact: true }).click()
await page.getByRole('option', { name: 'OpenRouter', exact: true }).click()
}
const model = page.getByRole('combobox', { name: 'C3 processing mode or model', exact: true })
await model.click()
await page.getByRole('option', { name: foreignModel, exact: true }).click()
}

test.use({ viewport: { width: 1440, height: 1000 } })

for (const scenario of [
{ name: 'new installation with missing flag', flag: undefined, legacy: false },
{ name: 'upgraded disabled flag', flag: false, legacy: true },
{ name: 'upgraded enabled flag', flag: true, legacy: true },
]) {
test(`${scenario.name} saves selected providers and retains them after reload`, async ({ page }, info) => {
const gateway = await installGateway(page, scenario.flag, scenario.legacy)
await page.goto('/control/settings/modelStrategy')
await expect(page.locator('.setup-model-strategy')).toBeVisible()
await expect(page.locator('.settings-dirtybar')).toHaveCount(0)
expect(gateway.writes).toHaveLength(0)
await selectForeignModel(page, scenario.legacy)
await page.locator('.settings-dirtybar').getByRole('button', { name: /^Save/ }).click()
await expect(page.locator('.settings-dirtybar')).toHaveCount(0)
expect(gateway.writes).toHaveLength(1)
expect(gateway.writes[0]).toMatchObject({
crossProviderTiers: true,
tiers: { c0: { provider: 'tokenrhythm', model: primaryModel }, c3: { provider: 'openrouter', model: foreignModel } },
})
await page.reload()
await expect(page.getByTestId('routing-cross-provider-enabled')).toBeVisible()
await expect(page.getByRole('combobox', { name: 'c3 request entry', exact: true })).toContainText('OpenRouter')
await expect(page.getByRole('combobox', { name: 'C3 processing mode or model', exact: true })).toHaveValue(foreignModel)
await expect(page.locator('.settings-dirtybar')).toHaveCount(0)
expect(gateway.writes).toHaveLength(1)
await page.screenshot({ path: info.outputPath('saved-providers.png'), animations: 'disabled' })
})
}

test('a rejected save keeps the selected providers available for retry', async ({ page }) => {
const gateway = await installGateway(page, false, false)
await page.goto('/control/settings/modelStrategy')
await expect(page.locator('.setup-model-strategy')).toBeVisible()
await selectForeignModel(page, false)
gateway.rejectNext()
const save = page.locator('.settings-dirtybar').getByRole('button', { name: /^Save/ })
await save.click()
await expect.poll(() => gateway.writes.length).toBe(1)
await expect(save).toBeEnabled()
await expect(page.getByRole('combobox', { name: 'C3 processing mode or model', exact: true })).toHaveValue(foreignModel)
expect(gateway.router.cross_provider_tiers).toBe(false)
await save.click()
await expect(page.locator('.settings-dirtybar')).toHaveCount(0)
expect(gateway.writes).toHaveLength(2)
expect(gateway.router.cross_provider_tiers).toBe(true)
})
14 changes: 7 additions & 7 deletions opensquilla-webui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion opensquilla-webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"@vitejs/plugin-vue": "^6.0.9",
"ajv": "8.20.0",
"fake-indexeddb": "6.2.5",
"happy-dom": "^20.14.5",
"happy-dom": "^21.0.4",
"json-schema-to-typescript": "16.0.0",
"json-schema-to-typescript-legacy": "npm:json-schema-to-typescript@15.0.4",
"typescript": "7.0.2",
Expand Down
Loading
Loading