Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
42acd23
feat(onboarding): verify connectivity without a model id
lifelmy Aug 31, 2026
74091f8
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 2, 2026
9445651
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 2, 2026
3c08ac0
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 2, 2026
a088944
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 3, 2026
64f14dc
style(onboarding): apply current formatting
Sep 3, 2026
31118a7
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 4, 2026
33de521
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 4, 2026
cd93a40
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 4, 2026
cfad87c
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 5, 2026
e3c1716
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 5, 2026
d7a086a
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 5, 2026
7416f76
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 6, 2026
b871d7e
Merge commit 'refs/task-a/upstream/main' into agent-tasks/792
Sep 7, 2026
924b79a
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 7, 2026
cb157be
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 7, 2026
8f4b410
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 7, 2026
a365363
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 8, 2026
8fed893
Merge branch 'main' of https://github.com/opensquilla/opensquilla int…
Sep 8, 2026
90943cb
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 8, 2026
bb5706d
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 9, 2026
1da9c14
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
115153d
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
057786a
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
cc03e40
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
876757f
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
02f1910
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
8fd74d1
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
540d77f
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
a2533a6
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
8601405
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
f0cc3ea
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
dd37868
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
6e3331a
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 10, 2026
5d3eec1
style(gateway): format onboarding RPC
Sep 11, 2026
9d4bc94
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 11, 2026
d7b8e12
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 11, 2026
f4ff410
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 11, 2026
6a2ead1
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 11, 2026
6b6a1e6
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 11, 2026
924c445
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 11, 2026
d7bc315
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 11, 2026
c68a10e
Merge remote-tracking branch 'upstream/main' into agent-tasks/792
Sep 12, 2026
5e45bea
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 13, 2026
4526e95
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 14, 2026
9e86985
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 14, 2026
9889fdd
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 14, 2026
9ebdac2
Merge commit 'refs/task-a/opensquilla/main' into agent-tasks/792
Sep 14, 2026
498c86f
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 14, 2026
e787de2
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 14, 2026
c6ba392
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 14, 2026
7f87c1f
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 14, 2026
d2b23fb
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
80ffcdb
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
f455127
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
d6ef4e8
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
4ae1101
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
9d96c53
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
a9baa13
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
6893c57
style(onboarding): apply current ruff formatting
Sep 15, 2026
a0e598e
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
79131bf
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
69ee5ac
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
a805a06
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 15, 2026
9d96887
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
9a84592
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
f24170a
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
20e52b0
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
0073702
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
1f0363b
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
bf0224a
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
404ff53
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
3aed3e0
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
2f3db2e
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
320c3f2
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
778824b
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 16, 2026
e91c919
Merge remote-tracking branch 'refs/remotes/upstream/main' into agent-…
Sep 17, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -6037,16 +6037,19 @@ describe('useSetupCatalog optional provider credentials', () => {
source: 'not_required',
probeReady: false,
})
// The model id no longer gates the probe (#792): only the still-empty
// non-model required field (Base URL) blocks it.
expect(credential?.probeDisabledReason).toBe(
'Complete required fields before verifying: Model, Base URL.',
'Complete required fields before verifying: Base URL.',
)

api.probeProviderConnection()
expect(rpcCall.mock.calls.some(call => call[0] === 'onboarding.provider.probe')).toBe(false)

api.updateProviderField('model', 'test-model')
api.updateProviderField('base_url', 'https://custom.example.test/v1')
credential = api.providerPanel.value.credentialPanel
// An empty model is now allowed: reachability is verified via the
// model-list endpoint.
expect(credential?.probeReady).toBe(true)
expect(credential?.probeDisabledReason).toBe('')

Expand All @@ -6055,7 +6058,6 @@ describe('useSetupCatalog optional provider credentials', () => {
expect(rpcCall).toHaveBeenCalledWith('onboarding.provider.probe', {
providerId,
baseUrl: 'https://custom.example.test/v1',
model: 'test-model',
})
app.unmount()
},
Expand Down
17 changes: 9 additions & 8 deletions opensquilla-webui/src/composables/setup/useSetupCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,19 +1548,20 @@ const providerProbeModel = computed(() => {

const providerProbeMissingFields = computed(() => {
if (!providerForm.selectedProvider.value) return []
// Stored/draft profiles probe through onboarding.llmProfile[.draft].probe,
// which still resolves a concrete deployment model, so keep requiring one.
if (providerSelectionKind.value === 'profile') {
return providerProbeModel.value ? [] : [t('setup.common.model')]
}
// For a draft primary-provider config the model id no longer gates the
// probe: an empty model makes onboarding.provider.probe verify reachability
// via the model-list endpoint instead of a chat turn (#792).
return providerFields.value
.filter(field => field.required === true && !isProviderCredentialField(field))
.filter(field => {
const value = field.name === 'model'
&& editingPrimaryProvider.value
&& hasConfiguredPrimaryProvider.value
? currentFormModelValue()
: providerForm.fieldValue(field, currentProviderConfig.value)
return !String(value ?? '').trim()
})
.filter(field => field.name !== 'model')
.filter(field => !String(
providerForm.fieldValue(field, currentProviderConfig.value) ?? '',
).trim())
.map(providerProbeFieldLabel)
})

Expand Down
156 changes: 65 additions & 91 deletions src/opensquilla/gateway/rpc_onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@


@contextmanager
def _validation_error(
code: str, *, router_provider_id: str | None = None
) -> Iterator[None]:
def _validation_error(code: str, *, router_provider_id: str | None = None) -> Iterator[None]:
"""Translate a mutation validation error into a stable, client-localizable
``RpcHandlerError`` code, keeping the original English text as the message so
the Web UI can fall back to it (and developers keep the detail).
Expand Down Expand Up @@ -115,6 +113,7 @@ def _channel_error() -> Iterator[None]:
details={"fields": details} if details else None,
) from exc


log = structlog.get_logger(__name__)

_d = get_dispatcher()
Expand Down Expand Up @@ -166,9 +165,7 @@ def __init__(self, config: Any, usage_event_sink: Any) -> None:
self._config = config
self._usage_event_sink = usage_event_sink

async def probe_primary(
self, command: ProbePrimaryProvider
) -> ProviderProbePayload:
async def probe_primary(self, command: ProbePrimaryProvider) -> ProviderProbePayload:
return cast(
"ProviderProbePayload",
await _probe_primary_provider(
Expand All @@ -186,9 +183,7 @@ async def discover_primary_models(
await _discover_primary_models(command, config=self._config),
)

async def discover_image_models(
self, provider_id: str
) -> ImageModelDiscoveryResult:
async def discover_image_models(self, provider_id: str) -> ImageModelDiscoveryResult:
return cast(
"ImageModelDiscoveryResult",
await _discover_image_models(provider_id),
Expand Down Expand Up @@ -250,9 +245,7 @@ def __init__(
self._connection_id = connection_id
self._usage_event_sink = usage_event_sink

async def probe_saved(
self, command: ProfileProbeCommand
) -> ProviderProbePayload:
async def probe_saved(self, command: ProfileProbeCommand) -> ProviderProbePayload:
return cast(
"ProviderProbePayload",
await _probe_saved_profile(
Expand All @@ -263,9 +256,7 @@ async def probe_saved(
),
)

async def probe_draft(
self, command: ProfileProbeCommand
) -> ProviderProbePayload:
async def probe_draft(self, command: ProfileProbeCommand) -> ProviderProbePayload:
return cast(
"ProviderProbePayload",
await _probe_draft_profile(
Expand All @@ -276,9 +267,7 @@ async def probe_draft(
),
)

async def discover_saved(
self, command: ProfileProbeCommand
) -> ProviderModelDiscoveryResult:
async def discover_saved(self, command: ProfileProbeCommand) -> ProviderModelDiscoveryResult:
return cast(
"ProviderModelDiscoveryResult",
await _discover_saved_profile_models(
Expand All @@ -288,9 +277,7 @@ async def discover_saved(
),
)

async def discover_draft(
self, command: ProfileProbeCommand
) -> ProviderModelDiscoveryResult:
async def discover_draft(self, command: ProfileProbeCommand) -> ProviderModelDiscoveryResult:
return cast(
"ProviderModelDiscoveryResult",
await _discover_draft_profile_models(
Expand Down Expand Up @@ -345,15 +332,11 @@ async def _models_discover(params: Any, ctx: RpcContext) -> dict[str, Any]:
)


async def _image_generation_models_discover(
params: Any, ctx: RpcContext
) -> dict[str, Any]:
async def _image_generation_models_discover(params: Any, ctx: RpcContext) -> dict[str, Any]:

return cast(
dict[str, Any],
await _provider_setup(ctx).discover_image_models(
str(_require(params, "providerId"))
),
await _provider_setup(ctx).discover_image_models(str(_require(params, "providerId"))),
)


Expand Down Expand Up @@ -393,23 +376,17 @@ async def _llm_profile_draft_probe(params: Any, ctx: RpcContext) -> dict[str, An
)


async def _llm_profile_models_discover(
params: Any, ctx: RpcContext
) -> dict[str, Any]:
async def _llm_profile_models_discover(params: Any, ctx: RpcContext) -> dict[str, Any]:
return cast(
dict[str, Any],
await _profile_lifecycle(ctx).discover_models(_profile_probe_command(params)),
)


async def _llm_profile_draft_models_discover(
params: Any, ctx: RpcContext
) -> dict[str, Any]:
async def _llm_profile_draft_models_discover(params: Any, ctx: RpcContext) -> dict[str, Any]:
return cast(
dict[str, Any],
await _profile_lifecycle(ctx).discover_draft_models(
_profile_probe_command(params)
),
await _profile_lifecycle(ctx).discover_draft_models(_profile_probe_command(params)),
)


Expand Down Expand Up @@ -484,9 +461,9 @@ def _request_changes_active_provider_connection(params: Any, cfg: Any) -> bool:
canonical_tokenrhythm_base_url,
)

requested_provider = str(
params.get("providerId") or getattr(llm, "provider", "") or ""
).strip().lower()
requested_provider = (
str(params.get("providerId") or getattr(llm, "provider", "") or "").strip().lower()
)

comparisons = (
("apiKey", "api_key"),
Expand Down Expand Up @@ -540,9 +517,7 @@ async def _provider_configure(params: Any, ctx: RpcContext) -> dict[str, Any]:
proxy=str(_param(params, "proxy", "")),
preset_id=str(_param(params, "presetId", "")),
router_action=str(_param(params, "routerAction", "preserve")),
image_generation_intent=str(
_param(params, "imageGenerationIntent", "preserve")
),
image_generation_intent=str(_param(params, "imageGenerationIntent", "preserve")),
)
result = await _provider_setup(ctx).configure_primary(command)
return cast(dict[str, Any], result.to_payload())
Expand Down Expand Up @@ -595,9 +570,7 @@ async def _llm_profile_upsert_and_activate(params: Any, ctx: RpcContext) -> dict
if not isinstance(params, dict) or any(value is None for value in params.values()):
# Generated optional Python fields use None for omission. The new
# wire Contract excludes explicit null so keep absence and clear distinct.
raise RpcHandlerError(
"INVALID_REQUEST", "Invalid save-and-activate profile parameters"
)
raise RpcHandlerError("INVALID_REQUEST", "Invalid save-and-activate profile parameters")
try:
p = validate_upsert_and_activate_params(params)
except ValidationError as exc:
Expand All @@ -614,9 +587,7 @@ async def _llm_profile_upsert_and_activate(params: Any, ctx: RpcContext) -> dict
api_key=p.get("apiKey"),
api_key_env=p.get("apiKeyEnv"),
api_key_env_pool=p.get("apiKeyEnvPool"),
keep_current_secret=p.get(
"keepCurrentSecret", p.get("preserveApiKey", False)
),
keep_current_secret=p.get("keepCurrentSecret", p.get("preserveApiKey", False)),
base_url=p.get("baseUrl"),
proxy=p.get("proxy"),
router_action=p.get("routerAction", "preserve"),
Expand Down Expand Up @@ -684,9 +655,7 @@ async def _llm_profile_active_remove(params: Any, ctx: RpcContext) -> dict[str,
replacement_provider_id = str(_require(params, "replacementProviderId"))
replacement_model = str(_param(params, "replacementModel", "") or "")
router_action = str(_param(params, "routerAction", "preserve"))
image_generation_intent = str(
_param(params, "imageGenerationIntent", "preserve")
)
image_generation_intent = str(_param(params, "imageGenerationIntent", "preserve"))
try:
result = await _profile_lifecycle(ctx).remove_active(
RemoveActiveProfile(
Expand All @@ -699,12 +668,8 @@ async def _llm_profile_active_remove(params: Any, ctx: RpcContext) -> dict[str,
)
except LlmProfileActivationError as exc:
code_by_reason = {
"primary_pool_unsupported": (
"onboarding.llmProfile.primary_pool_unsupported"
),
"router_provider_conflict": (
"onboarding.llmProfile.router_provider_conflict"
),
"primary_pool_unsupported": ("onboarding.llmProfile.primary_pool_unsupported"),
"router_provider_conflict": ("onboarding.llmProfile.router_provider_conflict"),
}
raise RpcHandlerError(
code_by_reason.get(exc.reason, "onboarding.llmProfile.invalid"),
Expand Down Expand Up @@ -763,12 +728,8 @@ async def _llm_profile_activate(params: Any, ctx: RpcContext) -> dict[str, Any]:
)
except LlmProfileActivationError as exc:
code_by_reason = {
"primary_pool_unsupported": (
"onboarding.llmProfile.primary_pool_unsupported"
),
"router_provider_conflict": (
"onboarding.llmProfile.router_provider_conflict"
),
"primary_pool_unsupported": ("onboarding.llmProfile.primary_pool_unsupported"),
"router_provider_conflict": ("onboarding.llmProfile.router_provider_conflict"),
}
code = code_by_reason.get(exc.reason, "onboarding.llmProfile.invalid")
details = {
Expand Down Expand Up @@ -1104,7 +1065,7 @@ async def _probe_primary_provider(
config: Any,
usage_event_sink: Any,
) -> dict[str, Any]:
"""Live one-token probe of a candidate provider config (nothing is saved)."""
"""Live probe of a candidate provider config without saving it."""
provider_id = command.provider_id
cfg = config
api_key = str(command.api_key or "")
Expand Down Expand Up @@ -1145,19 +1106,42 @@ async def _probe_primary_provider(
if not proxy:
proxy = str(getattr(cfg.llm, "proxy", "") or "")
model = str(command.model or "")
allow_default_api_key_env = not same_provider or reuse_stored_credentials
with _validation_error("onboarding.provider.invalid"):
result = await _usage_accounted_provider_probe(
usage_event_sink,
provider_id=str(provider_id),
model=model,
api_key=api_key,
api_key_env=api_key_env,
base_url=base_url,
proxy=proxy,
allow_default_api_key_env=(
not same_provider or reuse_stored_credentials
),
)
if model.strip():
result = await _usage_accounted_provider_probe(
usage_event_sink,
provider_id=str(provider_id),
model=model,
api_key=api_key,
api_key_env=api_key_env,
base_url=base_url,
proxy=proxy,
allow_default_api_key_env=allow_default_api_key_env,
)
else:
# A model is unnecessary for an endpoint/credential connectivity
# check; model discovery exercises that path without a chat turn.
from opensquilla.onboarding.probe import (
ProviderProbeResult,
discover_provider_models,
)

listing = await discover_provider_models(
provider_id=str(provider_id),
api_key=api_key,
api_key_env=api_key_env,
base_url=base_url,
proxy=proxy,
allow_default_api_key_env=allow_default_api_key_env,
)
result = ProviderProbeResult(
ok=listing.ok,
provider_id=str(provider_id),
model="",
failure_kind=listing.failure_kind,
message=listing.detail,
)
saved_model = str(getattr(cfg.llm, "model", "") or "").strip()
if (
same_provider
Expand Down Expand Up @@ -1277,13 +1261,9 @@ async def _discover_primary_models(
api_key_env=api_key_env,
base_url=base_url,
proxy=proxy,
allow_default_api_key_env=(
not same_provider or reuse_stored_credentials
),
allow_default_api_key_env=(not same_provider or reuse_stored_credentials),
force_refresh=force_refresh,
persist_catalog=(
same_provider and reuse_stored_credentials and not request_overrides
),
persist_catalog=(same_provider and reuse_stored_credentials and not request_overrides),
catalog_config=cfg,
)
return result.to_payload()
Expand Down Expand Up @@ -1386,9 +1366,7 @@ async def _channel_probe(params: Any, ctx: RpcContext) -> dict[str, Any]:
"probeKind": "local_validation",
"restartRequired": True,
"entry": redact_channel_entry(type_name, normalized),
"warnings": [
"Configuration is locally valid; no provider connection was attempted."
],
"warnings": ["Configuration is locally valid; no provider connection was attempted."],
}


Expand Down Expand Up @@ -1481,9 +1459,7 @@ async def _audio_configure(params: Any, ctx: RpcContext) -> dict[str, Any]:
async def _capability_reset(params: Any, ctx: RpcContext) -> dict[str, Any]:

with _validation_error("onboarding.capability.invalid"):
result = await _capability_setup(ctx).reset(
str(_require(params, "capabilityId"))
)
result = await _capability_setup(ctx).reset(str(_require(params, "capabilityId")))
return cast(dict[str, Any], result.to_payload())


Expand Down Expand Up @@ -1622,9 +1598,7 @@ async def _channel_disable(params: Any, ctx: RpcContext) -> dict[str, Any]:
"onboarding.llmProfile.probe": _llm_profile_probe,
"onboarding.llmProfile.draft.probe": _llm_profile_draft_probe,
"onboarding.llmProfile.models.discover": _llm_profile_models_discover,
"onboarding.llmProfile.draft.models.discover": (
_llm_profile_draft_models_discover
),
"onboarding.llmProfile.draft.models.discover": (_llm_profile_draft_models_discover),
"onboarding.router.configure": _router_configure,
"onboarding.ensemble.configure": _ensemble_configure,
"onboarding.search.configure": _search_configure,
Expand Down
Loading
Loading