Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bfbd9b9bc1 |
@@ -63,10 +63,14 @@ _PI_PROVIDER_ID = "omnigent"
|
||||
# carries no explicit model override.
|
||||
_DATABRICKS_PI_DEFAULT_MODEL = "databricks-claude-sonnet-4-6"
|
||||
|
||||
# Provider id for the secondary OpenAI Completions provider registered alongside
|
||||
# the primary Anthropic provider in Databricks gateway configs.
|
||||
# Provider id for the secondary OpenAI Responses provider (GPT models that only
|
||||
# support tools via the Responses API, e.g. gpt-5.5, gpt-5.6-*).
|
||||
_PI_OPENAI_PROVIDER_ID = "omnigent-openai"
|
||||
|
||||
# Provider id for the tertiary OpenAI Completions provider (non-GPT models that
|
||||
# work via /chat/completions: Kimi, Llama, GLM, Gemini, older GPT models).
|
||||
_PI_COMPLETIONS_PROVIDER_ID = "omnigent-completions"
|
||||
|
||||
# Databricks AI Gateway Anthropic Messages surface. Pi speaks this protocol
|
||||
# natively (``api: anthropic-messages``); the gateway authenticates with a
|
||||
# workspace bearer token, so we set ``authHeader`` (Authorization: Bearer).
|
||||
@@ -224,13 +228,25 @@ def _databricks_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiPro
|
||||
from omnigent.runtime.credentials.databricks import resolve_databricks_workspace
|
||||
|
||||
creds = resolve_databricks_workspace(entry.profile)
|
||||
claude_models, openai_models = _fetch_pi_model_lists(creds.host, creds.token)
|
||||
claude_models, gpt_models, completions_models = _fetch_pi_model_lists(
|
||||
creds.host, creds.token
|
||||
)
|
||||
except Exception: # noqa: BLE001 — credential/network failure must not break launch
|
||||
_LOGGER.info(
|
||||
"pi-native: falling back to single-model display (could not resolve credentials)"
|
||||
)
|
||||
claude_models = []
|
||||
openai_models = []
|
||||
gpt_models = []
|
||||
completions_models = []
|
||||
additional: dict[str, Any] = {}
|
||||
if gpt_models:
|
||||
additional[_PI_OPENAI_PROVIDER_ID] = _databricks_openai_provider(
|
||||
api_key, f"{host}/ai-gateway/codex/v1", gpt_models
|
||||
)
|
||||
if completions_models:
|
||||
additional[_PI_COMPLETIONS_PROVIDER_ID] = _databricks_openai_provider(
|
||||
api_key, f"{host}/serving-endpoints", completions_models, api_type="openai-completions"
|
||||
)
|
||||
return PiProviderConfig(
|
||||
provider_id=_PI_PROVIDER_ID,
|
||||
base_url=f"{host}{_DATABRICKS_ANTHROPIC_GATEWAY_PATH}",
|
||||
@@ -242,33 +258,34 @@ def _databricks_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiPro
|
||||
api_key=api_key,
|
||||
auth_header=True,
|
||||
extra_models=claude_models,
|
||||
additional_providers=(
|
||||
{
|
||||
_PI_OPENAI_PROVIDER_ID: _databricks_openai_provider(
|
||||
api_key, f"{host}/serving-endpoints", openai_models
|
||||
)
|
||||
}
|
||||
if openai_models
|
||||
else {}
|
||||
),
|
||||
additional_providers=additional,
|
||||
)
|
||||
|
||||
|
||||
def _databricks_openai_provider(
|
||||
api_key: str,
|
||||
serving_endpoints_url: str,
|
||||
base_url: str,
|
||||
models: list[dict[str, Any]],
|
||||
api_type: str = "openai-responses",
|
||||
) -> dict[str, Any]:
|
||||
"""Build a Pi OpenAI Completions provider config for the Databricks gateway.
|
||||
"""Build a Pi OpenAI provider config for Databricks models.
|
||||
|
||||
GPT models on the Databricks workspace are served via the OpenAI
|
||||
Completions API at ``/serving-endpoints``. The ``compat`` block disables
|
||||
OpenAI-specific features the Databricks endpoint doesn't support.
|
||||
``api_type`` selects the wire protocol:
|
||||
|
||||
* ``"openai-responses"`` — AI Gateway codex surface
|
||||
(``/ai-gateway/codex/v1``). Required for newer GPT models (gpt-5.5,
|
||||
gpt-5.6-*) that reject function tool calls via ``/chat/completions``.
|
||||
* ``"openai-completions"`` — workspace serving-endpoints surface. Works
|
||||
for Kimi, Llama, GLM, Gemini, and older GPT models.
|
||||
|
||||
``authHeader`` sends ``Authorization: Bearer {token}`` (Databricks requires
|
||||
this; without it the OpenAI SDK uses ``api-key`` which is rejected).
|
||||
"""
|
||||
return {
|
||||
"baseUrl": serving_endpoints_url,
|
||||
"baseUrl": base_url,
|
||||
"apiKey": api_key,
|
||||
"api": "openai-completions",
|
||||
"api": api_type,
|
||||
"authHeader": True,
|
||||
"compat": {
|
||||
"supportsDeveloperRole": False,
|
||||
"supportsStore": False,
|
||||
@@ -309,19 +326,38 @@ def _run_auth_command(auth_command: str, *, timeout: float = 15.0) -> str | None
|
||||
return None
|
||||
|
||||
|
||||
def _needs_responses_api(model_id_lower: str) -> bool:
|
||||
"""Return True when a Databricks model requires the Responses API for tools.
|
||||
|
||||
Newer GPT models (gpt-5.5, gpt-5.6-*, gpt-5.3-codex) reject function tool
|
||||
calls via ``/chat/completions`` with 400; they work via the Responses API at
|
||||
the AI Gateway (``/ai-gateway/codex/v1/responses``). Detected by name: these
|
||||
models have ``gpt-5.5``, ``gpt-5.6``, or ``gpt-5.3-codex`` in their id.
|
||||
Non-GPT models (Kimi, Llama, GLM, Gemini) and older GPT (5.4, 5.2, …) work
|
||||
fine with ``/chat/completions`` + tools.
|
||||
|
||||
Expects a pre-lowercased model id (the caller typically has ``name_lower``
|
||||
already computed).
|
||||
"""
|
||||
return any(token in model_id_lower for token in ("gpt-5-5", "gpt-5-6", "gpt-5-3-codex"))
|
||||
|
||||
|
||||
def _fetch_pi_model_lists(
|
||||
workspace_url: str,
|
||||
token: str,
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""Fetch live model lists from the Databricks serving-endpoints API.
|
||||
|
||||
Calls ``GET <workspace>/api/2.0/serving-endpoints``, filters for READY LLM
|
||||
endpoints, and splits them into two Pi model entry dict lists:
|
||||
|
||||
* Claude models → ``anthropic-messages`` provider.
|
||||
* All other LLMs (GPT, GLM, Llama, Qwen, Kimi, …) → ``openai-completions``
|
||||
provider. All non-Claude Databricks LLMs share the same serving-endpoints
|
||||
URL and wire protocol, so a single provider covers them all.
|
||||
* Newer GPT models (gpt-5.5, gpt-5.6-*, gpt-5.3-codex, …) that reject
|
||||
function tools via ``/chat/completions`` → ``openai-responses`` provider
|
||||
at the AI Gateway codex surface.
|
||||
* Other LLMs (Kimi, Llama, GLM, Gemini, older GPT …) that work with
|
||||
function tools via ``/chat/completions`` → ``openai-completions`` provider
|
||||
at the serving-endpoints surface.
|
||||
|
||||
Falls back to empty lists on any HTTP or auth failure so a network blip
|
||||
never breaks Pi session launch.
|
||||
@@ -329,8 +365,8 @@ def _fetch_pi_model_lists(
|
||||
:param workspace_url: Databricks workspace base URL, e.g.
|
||||
``"https://wkspc.example.com"`` — **no** trailing slash or path.
|
||||
:param token: Bearer token for the workspace API.
|
||||
:returns: ``(claude_models, openai_models)`` — Pi model entry dicts ready
|
||||
to write into ``models.json``.
|
||||
:returns: ``(claude_models, gpt_responses_models, completions_models)`` —
|
||||
Pi model entry dicts ready to write into ``models.json``.
|
||||
"""
|
||||
import httpx
|
||||
|
||||
@@ -348,14 +384,16 @@ def _fetch_pi_model_lists(
|
||||
"Pi will show only the selected model",
|
||||
exc_info=True,
|
||||
)
|
||||
return [], []
|
||||
return [], [], []
|
||||
|
||||
endpoints = payload.get("endpoints") if isinstance(payload, dict) else None
|
||||
claude: list[dict[str, Any]] = []
|
||||
# All non-Claude LLM models (GPT, GLM, Llama, Qwen, Kimi, …) route to the
|
||||
# same OpenAI Completions provider and serving-endpoints URL, so they share
|
||||
# one list regardless of model family.
|
||||
openai: list[dict[str, Any]] = []
|
||||
# Newer GPT models (gpt-5.5, gpt-5.6-*, gpt-5.3-codex) reject function tools
|
||||
# via /chat/completions; they need the Responses API at the AI Gateway.
|
||||
gpt_responses: list[dict[str, Any]] = []
|
||||
# Non-GPT models (Kimi, Llama, GLM, Gemini) and older GPT models work fine
|
||||
# with function tools via /chat/completions at serving-endpoints.
|
||||
completions: list[dict[str, Any]] = []
|
||||
|
||||
for endpoint in endpoints if isinstance(endpoints, list) else []:
|
||||
if not isinstance(endpoint, dict):
|
||||
@@ -383,16 +421,18 @@ def _fetch_pi_model_lists(
|
||||
entry: dict[str, Any] = {"id": name, "input": ["text", "image"]}
|
||||
if "claude" in name_lower:
|
||||
claude.append(entry)
|
||||
elif _needs_responses_api(name_lower):
|
||||
gpt_responses.append(entry)
|
||||
else:
|
||||
openai.append(entry)
|
||||
completions.append(entry)
|
||||
|
||||
if not claude and not openai:
|
||||
if not claude and not gpt_responses and not completions:
|
||||
_LOGGER.info(
|
||||
"pi-native: Databricks serving-endpoints returned no LLM models; "
|
||||
"Pi will show only the selected model"
|
||||
)
|
||||
|
||||
return claude, openai
|
||||
return claude, gpt_responses, completions
|
||||
|
||||
|
||||
def _gateway_anthropic_base_url(codex_base_url: str) -> str:
|
||||
@@ -567,7 +607,8 @@ def _cli_config_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiPro
|
||||
# the API call. The SDK's minted token may not have serving-endpoints
|
||||
# access on workspaces where access is controlled via the auth command.
|
||||
claude_models: list[dict[str, Any]] = []
|
||||
openai_models: list[dict[str, Any]] = []
|
||||
gpt_models: list[dict[str, Any]] = []
|
||||
completions_models: list[dict[str, Any]] = []
|
||||
# Derive the workspace URL for the serving-endpoints API call.
|
||||
# For dedicated-subdomain URLs (ai-gateway.cloud.databricks.com), the
|
||||
# real workspace hostname must come from ~/.databrickscfg. For
|
||||
@@ -593,20 +634,38 @@ def _cli_config_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiPro
|
||||
if real_workspace_url and transport.auth_command:
|
||||
token = _run_auth_command(transport.auth_command)
|
||||
if token:
|
||||
claude_models, openai_models = _fetch_pi_model_lists(real_workspace_url, token)
|
||||
claude_models, gpt_models, completions_models = _fetch_pi_model_lists(
|
||||
real_workspace_url, token
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"pi-native: auth command produced no token; Pi will show only the selected model"
|
||||
)
|
||||
additional: dict[str, Any] = (
|
||||
{
|
||||
_PI_OPENAI_PROVIDER_ID: _databricks_openai_provider(
|
||||
api_key, f"{real_workspace_url}/serving-endpoints", openai_models
|
||||
)
|
||||
}
|
||||
if real_workspace_url and openai_models
|
||||
else {}
|
||||
# Derive the AI Gateway codex URL for the openai-responses provider. For
|
||||
# workspace-hosted URLs the transport base is already the codex path;
|
||||
# for dedicated-subdomain URLs we build it from the workspace URL.
|
||||
if _DATABRICKS_AI_GATEWAY_LABEL in gateway_labels:
|
||||
# Dedicated subdomain: transport.base_url is the codex gateway URL.
|
||||
# Strip trailing path suffixes to get the codex base, not /anthropic.
|
||||
codex_gateway_url = transport.base_url.rstrip("/")
|
||||
if codex_gateway_url.endswith(_DATABRICKS_GATEWAY_CODEX_SUFFIX):
|
||||
codex_gateway_url = codex_gateway_url[: -len(_DATABRICKS_GATEWAY_CODEX_SUFFIX)]
|
||||
codex_gateway_url = f"{codex_gateway_url}{_DATABRICKS_GATEWAY_CODEX_SUFFIX}"
|
||||
else:
|
||||
# Workspace-hosted gateway: build from workspace hostname.
|
||||
codex_gateway_url = f"https://{parsed_gateway.hostname}/ai-gateway/codex/v1"
|
||||
workspace_completions_url = (
|
||||
real_workspace_url + "/serving-endpoints" if real_workspace_url else None
|
||||
)
|
||||
additional: dict[str, Any] = {}
|
||||
if gpt_models:
|
||||
additional[_PI_OPENAI_PROVIDER_ID] = _databricks_openai_provider(
|
||||
api_key, codex_gateway_url, gpt_models
|
||||
)
|
||||
if completions_models and workspace_completions_url:
|
||||
additional[_PI_COMPLETIONS_PROVIDER_ID] = _databricks_openai_provider(
|
||||
api_key, workspace_completions_url, completions_models, api_type="openai-completions"
|
||||
)
|
||||
return PiProviderConfig(
|
||||
provider_id=_PI_PROVIDER_ID,
|
||||
base_url=_gateway_anthropic_base_url(transport.base_url),
|
||||
|
||||
@@ -20515,26 +20515,27 @@ def create_sessions_router(
|
||||
)
|
||||
except Exception: # noqa: BLE001 -- best-effort snapshot; never block live tail
|
||||
_logger.debug("snapshot: child sessions failed for %s", session_id, exc_info=True)
|
||||
try:
|
||||
resp = await asyncio.wait_for(
|
||||
# order=asc: the web cache appends each replayed
|
||||
# ``created`` event, so the replay must arrive in
|
||||
# creation order or the session's own terminal (always
|
||||
# created first) lands behind later agent-launched
|
||||
# ones. limit=1000 (the runner endpoint max) keeps the
|
||||
# oldest-first window from dropping the newest
|
||||
# terminals past the default page of 20.
|
||||
runner_client.get(
|
||||
f"/v1/sessions/{session_id}/resources/terminals",
|
||||
params={"order": "asc", "limit": "1000"},
|
||||
),
|
||||
timeout=_SNAPSHOT_RUNNER_TIMEOUT_S,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
for item in resp.json().get("data", []):
|
||||
events.append({"type": "session.resource.created", "resource": item})
|
||||
except Exception: # noqa: BLE001 -- best-effort snapshot; never block live tail
|
||||
_logger.debug("snapshot: terminals failed for %s", session_id, exc_info=True)
|
||||
if runner_client is not None:
|
||||
try:
|
||||
resp = await asyncio.wait_for(
|
||||
# order=asc: the web cache appends each replayed
|
||||
# ``created`` event, so the replay must arrive in
|
||||
# creation order or the session's own terminal (always
|
||||
# created first) lands behind later agent-launched
|
||||
# ones. limit=1000 (the runner endpoint max) keeps the
|
||||
# oldest-first window from dropping the newest
|
||||
# terminals past the default page of 20.
|
||||
runner_client.get(
|
||||
f"/v1/sessions/{session_id}/resources/terminals",
|
||||
params={"order": "asc", "limit": "1000"},
|
||||
),
|
||||
timeout=_SNAPSHOT_RUNNER_TIMEOUT_S,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
for item in resp.json().get("data", []):
|
||||
events.append({"type": "session.resource.created", "resource": item})
|
||||
except Exception: # noqa: BLE001 -- best-effort snapshot; never block live tail
|
||||
_logger.debug("snapshot: terminals failed for %s", session_id, exc_info=True)
|
||||
# Tell the client to (re)fetch the changed-files list rather
|
||||
# than fetching it here (avoids a second runner round-trip).
|
||||
events.append(
|
||||
|
||||
@@ -845,19 +845,31 @@ def test_databricks_profile_registers_gpt_provider(monkeypatch: pytest.MonkeyPat
|
||||
"resolve_databricks_workspace",
|
||||
lambda profile: db_creds_mod.WorkspaceCreds(host="https://wkspc.example.com", token="tok"),
|
||||
)
|
||||
live_gpt = [{"id": "databricks-gpt-5-4", "input": ["text", "image"]}]
|
||||
# gpt-5-5 needs the Responses API; gpt-5-4 uses Completions
|
||||
live_gpt_responses = [{"id": "databricks-gpt-5-5", "input": ["text", "image"]}]
|
||||
live_gpt_completions = [{"id": "databricks-gpt-5-4", "input": ["text", "image"]}]
|
||||
live_claude = [{"id": "databricks-claude-sonnet-4-6", "input": ["text", "image"]}]
|
||||
monkeypatch.setattr(creds, "_fetch_pi_model_lists", lambda *_: (live_claude, live_gpt))
|
||||
monkeypatch.setattr(
|
||||
creds,
|
||||
"_fetch_pi_model_lists",
|
||||
lambda *_: (live_claude, live_gpt_responses, live_gpt_completions),
|
||||
)
|
||||
|
||||
provider = creds.resolve_pi_native_provider(config_loader=_databricks_config)
|
||||
assert provider is not None
|
||||
|
||||
cfg = provider.to_models_config()
|
||||
openai_entry = cfg["providers"].get("omnigent-openai")
|
||||
assert openai_entry is not None, "omnigent-openai provider missing from models.json"
|
||||
assert openai_entry["baseUrl"] == "https://wkspc.example.com/serving-endpoints"
|
||||
assert openai_entry["api"] == "openai-completions"
|
||||
assert any(m["id"] == "databricks-gpt-5-4" for m in openai_entry["models"])
|
||||
assert openai_entry is not None, (
|
||||
"omnigent-openai (responses) provider missing from models.json"
|
||||
)
|
||||
assert openai_entry["baseUrl"] == "https://wkspc.example.com/ai-gateway/codex/v1"
|
||||
assert openai_entry["api"] == "openai-responses"
|
||||
assert any(m["id"] == "databricks-gpt-5-5" for m in openai_entry["models"])
|
||||
completions_entry = cfg["providers"].get("omnigent-completions")
|
||||
assert completions_entry is not None, "omnigent-completions provider missing from models.json"
|
||||
assert completions_entry["api"] == "openai-completions"
|
||||
assert any(m["id"] == "databricks-gpt-5-4" for m in completions_entry["models"])
|
||||
|
||||
|
||||
def test_cli_config_databricks_registers_gpt_provider(
|
||||
@@ -893,7 +905,7 @@ def test_cli_config_databricks_registers_gpt_provider(
|
||||
# Assert the auth_command token is used, not the SDK token
|
||||
assert token == "cmd-tok", f"expected auth_command token, got {token!r}"
|
||||
assert "dbc-a5d4177a" in workspace_url
|
||||
return live_claude, live_gpt
|
||||
return live_claude, live_gpt, []
|
||||
|
||||
monkeypatch.setattr(creds, "_fetch_pi_model_lists", _mock_fetch)
|
||||
|
||||
@@ -903,13 +915,13 @@ def test_cli_config_databricks_registers_gpt_provider(
|
||||
cfg = provider.to_models_config()
|
||||
openai_entry = cfg["providers"].get("omnigent-openai")
|
||||
assert openai_entry is not None, "omnigent-openai provider missing from models.json"
|
||||
# The serving-endpoints URL uses the REAL workspace hostname from databrickscfg,
|
||||
# not a derived gateway hostname (which would be NXDOMAIN).
|
||||
# Uses the AI Gateway codex URL (supports tools); the REAL workspace hostname
|
||||
# from databrickscfg fixes the NXDOMAIN issue for dedicated-subdomain gateways.
|
||||
assert (
|
||||
openai_entry["baseUrl"]
|
||||
== "https://dbc-a5d4177a-49dc.cloud.databricks.com/serving-endpoints"
|
||||
== "https://1965859176160743.ai-gateway.cloud.databricks.com/codex/v1"
|
||||
)
|
||||
assert openai_entry["api"] == "openai-completions"
|
||||
assert openai_entry["api"] == "openai-responses"
|
||||
assert any(m["id"] == "databricks-gpt-5-4" for m in openai_entry["models"])
|
||||
|
||||
|
||||
@@ -943,7 +955,13 @@ def test_fetch_pi_model_lists_parses_serving_endpoints() -> None:
|
||||
# GLM without task field — falls back to name token "glm"
|
||||
{"name": "databricks-glm-4-7", "state": {"ready": "READY"}},
|
||||
{"name": "my-embedding-model", "task": "llm/v1/embeddings"},
|
||||
{"name": "databricks-gpt-5-5", "task": "llm/v1/chat", "state": {"ready": "NOT_READY"}},
|
||||
{"name": "databricks-gpt-5-5", "task": "llm/v1/chat", "state": {"ready": "READY"}},
|
||||
# NOT_READY — excluded regardless of API type
|
||||
{
|
||||
"name": "databricks-gpt-5-5-pro",
|
||||
"task": "llm/v1/chat",
|
||||
"state": {"ready": "NOT_READY"},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -958,22 +976,25 @@ def test_fetch_pi_model_lists_parses_serving_endpoints() -> None:
|
||||
"httpx.Client",
|
||||
lambda **kw: _real_client(transport=_MockTransport()),
|
||||
):
|
||||
claude, openai = creds._fetch_pi_model_lists("https://wkspc.example.com", "tok")
|
||||
claude, gpt, completions = creds._fetch_pi_model_lists("https://wkspc.example.com", "tok")
|
||||
|
||||
assert [m["id"] for m in claude] == [
|
||||
"databricks-claude-sonnet-4-6",
|
||||
"databricks-claude-opus-4-8",
|
||||
]
|
||||
# GPT, Llama, and GLM (both task-detected and name-detected) all go to openai
|
||||
openai_ids = [m["id"] for m in openai]
|
||||
assert "databricks-gpt-5-4" in openai_ids
|
||||
assert "databricks-llama-3-70b" in openai_ids
|
||||
assert "databricks-zai-org-glm-4-7" in openai_ids
|
||||
assert "databricks-glm-4-7" in openai_ids
|
||||
# Newer GPT needing Responses API
|
||||
gpt_ids = [m["id"] for m in gpt]
|
||||
assert "databricks-gpt-5-5" in gpt_ids # needs responses API
|
||||
assert "databricks-gpt-5-4" not in gpt_ids # works with completions
|
||||
# Llama and GLM go to completions (work with /chat/completions + tools)
|
||||
completions_ids = [m["id"] for m in completions]
|
||||
assert "databricks-gpt-5-4" in completions_ids
|
||||
assert "databricks-llama-3-70b" in completions_ids
|
||||
assert "databricks-zai-org-glm-4-7" in completions_ids
|
||||
assert "databricks-glm-4-7" in completions_ids
|
||||
# Embeddings and not-ready endpoints excluded
|
||||
assert "my-embedding-model" not in openai_ids
|
||||
assert "databricks-gpt-5-5" not in openai_ids
|
||||
assert all(m.get("input") == ["text", "image"] for m in claude + openai)
|
||||
assert "my-embedding-model" not in gpt_ids + completions_ids
|
||||
assert all(m.get("input") == ["text", "image"] for m in claude + gpt + completions)
|
||||
|
||||
|
||||
def test_fetch_pi_model_lists_falls_back_on_http_error() -> None:
|
||||
@@ -995,7 +1016,10 @@ def test_fetch_pi_model_lists_falls_back_on_http_error() -> None:
|
||||
"httpx.Client",
|
||||
lambda **kw: _real_client(transport=_ErrorTransport()),
|
||||
):
|
||||
claude, openai = creds._fetch_pi_model_lists("https://wkspc.example.com", "bad-tok")
|
||||
claude, gpt, completions = creds._fetch_pi_model_lists(
|
||||
"https://wkspc.example.com", "bad-tok"
|
||||
)
|
||||
|
||||
assert claude == []
|
||||
assert openai == []
|
||||
assert gpt == []
|
||||
assert completions == []
|
||||
|
||||
@@ -96,7 +96,6 @@ import {
|
||||
import { useAutoGrowTextarea } from "@/hooks/useAutoGrowTextarea";
|
||||
import { useRecentWorkspaces } from "@/hooks/useRecentWorkspaces";
|
||||
import { useDirectorySessions } from "@/hooks/useDirectorySessions";
|
||||
import { useRunnerHealthRegistration } from "@/hooks/RunnerHealthProvider";
|
||||
import { useHostFilesystem, type HostFilesystemEntry } from "@/hooks/useHostFilesystem";
|
||||
import { useHostWorktrees } from "@/hooks/useHostWorktrees";
|
||||
import { useNativeServerSwitcherForMainSurface } from "@/hooks/useNativeServerSwitcher";
|
||||
@@ -1730,9 +1729,6 @@ export function NewChatLandingScreen() {
|
||||
const { data: agents } = useAvailableAgents();
|
||||
const brainHarnessLabels = useBrainHarnessLabels();
|
||||
const { data: hosts, isLoading: hostsLoading } = useHosts();
|
||||
// Sessions the caller can access, to warn when a new session would share a
|
||||
// working directory with a live one (see the conflict tooltip below).
|
||||
const { data: directorySessions } = useDirectorySessions(true);
|
||||
|
||||
const agentList = useMemo(
|
||||
() =>
|
||||
@@ -1844,6 +1840,9 @@ export function NewChatLandingScreen() {
|
||||
const [selectedHostId, setSelectedHostId] = useState<string | null>(
|
||||
() => landingDraft?.selectedHostId ?? null,
|
||||
);
|
||||
// Sessions on the selected host — fetched only when a host is selected,
|
||||
// to avoid registering hundreds of sessions into the health poll at idle.
|
||||
const { data: directorySessions } = useDirectorySessions(selectedHostId !== null);
|
||||
// True when the user picked the sandbox option instead of a connected
|
||||
// host — the server provisions a sandbox host at create time
|
||||
// (host_type: "managed"), so no host_id or workspace is sent.
|
||||
@@ -2216,27 +2215,21 @@ export function NewChatLandingScreen() {
|
||||
const isCloudHost =
|
||||
sandboxSelected || (selectedHost?.name?.toLowerCase().includes("cloud") ?? false);
|
||||
|
||||
// Sessions on the selected host that have a workspace — candidates for a
|
||||
// directory conflict, fed to the runner-health poll so only *connected*
|
||||
// agents count (same /health signal as the sidebar dots).
|
||||
const conflictCandidates = useMemo(
|
||||
() =>
|
||||
(directorySessions ?? []).filter((s) => s.host_id === selectedHostId && s.workspace != null),
|
||||
[directorySessions, selectedHostId],
|
||||
);
|
||||
const runnerHealth = useRunnerHealthRegistration(conflictCandidates);
|
||||
// Count of live agents per normalized directory on this host. The file
|
||||
// browser uses this to warn when you navigate into an occupied directory.
|
||||
// Uses runner_online from the session list response directly — good enough
|
||||
// for a conflict hint without registering hundreds of sessions into the
|
||||
// /health poll.
|
||||
const occupancyByDir = useMemo(() => {
|
||||
const counts = new Map<string, number>();
|
||||
for (const s of conflictCandidates) {
|
||||
if (s.workspace == null || runnerHealth.get(s.id) !== true) continue;
|
||||
for (const s of directorySessions ?? []) {
|
||||
if (s.host_id !== selectedHostId || s.workspace == null || s.runner_online !== true) continue;
|
||||
const dir = normalizeWorkspacePath(s.workspace);
|
||||
if (dir === null) continue;
|
||||
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
||||
}
|
||||
return counts;
|
||||
}, [conflictCandidates, runnerHealth]);
|
||||
}, [directorySessions, selectedHostId]);
|
||||
|
||||
// Existing git worktrees of the picked directory's repo, for the
|
||||
// worktree picker. Skipped for sandbox sessions (server-managed) and
|
||||
@@ -2426,17 +2419,15 @@ export function NewChatLandingScreen() {
|
||||
if (mentionFsQuery.isPlaceholderData) return [];
|
||||
const rows = (mentionFsQuery.data?.entries ?? [])
|
||||
.filter((e) => e.type === "directory" || e.type === "file")
|
||||
.map(
|
||||
(e): WorkspaceFile => ({
|
||||
path: e.path.startsWith(workspaceRoot)
|
||||
? e.path.slice(workspaceRoot.length).replace(/^\/+/, "")
|
||||
: e.name,
|
||||
name: e.name,
|
||||
type: e.type === "directory" ? "directory" : "file",
|
||||
bytes: e.bytes,
|
||||
modified_at: e.modified_at,
|
||||
}),
|
||||
);
|
||||
.map((e): WorkspaceFile => ({
|
||||
path: e.path.startsWith(workspaceRoot)
|
||||
? e.path.slice(workspaceRoot.length).replace(/^\/+/, "")
|
||||
: e.name,
|
||||
name: e.name,
|
||||
type: e.type === "directory" ? "directory" : "file",
|
||||
bytes: e.bytes,
|
||||
modified_at: e.modified_at,
|
||||
}));
|
||||
return rankMentionEntries(rows, mentionFilter);
|
||||
}, [
|
||||
mentionEnabled,
|
||||
|
||||
Reference in New Issue
Block a user