508 lines
23 KiB
Python
508 lines
23 KiB
Python
"""Translate the omnigent-configured model provider into native Pi config.
|
|
|
|
A native Pi session launches the ``pi`` CLI, which authenticates from its own
|
|
config directory (``~/.pi/agent``). Without help, a user who ran ``omnigent
|
|
setup`` would still have to run ``pi`` ``/login`` separately — unlike
|
|
claude-native / codex-native, which route through the provider that ``omnigent
|
|
setup`` configured.
|
|
|
|
This module closes that gap. It resolves the provider configured for the Pi
|
|
surface (``~/.omnigent/config.yaml``) and writes a per-session ``models.json``
|
|
into a *managed* Pi config dir (selected via ``PI_CODING_AGENT_DIR``), so the
|
|
runner-owned ``pi`` process authenticates exactly like the configured harness —
|
|
mirroring how codex-native routes through the Databricks AI Gateway.
|
|
|
|
The managed config dir is per-session (like codex-native's managed
|
|
``CODEX_HOME``), so this never mutates the user's global ``~/.pi/agent``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
from urllib.parse import urlparse
|
|
|
|
from omnigent.model_override import normalize_model_for_provider
|
|
from omnigent.onboarding.provider_config import (
|
|
CHAT_WIRE_API,
|
|
CLI_CONFIG_KIND,
|
|
DATABRICKS_KIND,
|
|
GATEWAY_KIND,
|
|
KEY_KIND,
|
|
LOCAL_KIND,
|
|
PI_SURFACE,
|
|
ProviderEntry,
|
|
default_provider_for_harness,
|
|
load_config,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
# Annotation-only import (the runtime import is lazy inside the function,
|
|
# since ``ambient`` pulls in onboarding-only deps this module avoids on the
|
|
# runner's session-create hot path).
|
|
from omnigent.onboarding.ambient import CodexConfigTransport
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# Env var the ``pi`` CLI reads to relocate its config dir (default
|
|
# ``~/.pi/agent``). Setting it per session gives Pi a managed, isolated
|
|
# config dir we own — the analog of codex-native's ``CODEX_HOME``.
|
|
PI_CODING_AGENT_DIR_ENV_VAR = "PI_CODING_AGENT_DIR"
|
|
|
|
# Provider id registered in the generated ``models.json``. Stable so
|
|
# ``--provider`` can select it.
|
|
_PI_PROVIDER_ID = "omnigent"
|
|
|
|
# Default model for the Databricks AI Gateway's Anthropic surface — the same
|
|
# default the in-process Databricks executor pins. Used when the session
|
|
# carries no explicit model override.
|
|
_DATABRICKS_PI_DEFAULT_MODEL = "databricks-claude-sonnet-4-6"
|
|
|
|
# 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).
|
|
_DATABRICKS_ANTHROPIC_GATEWAY_PATH = "/ai-gateway/anthropic"
|
|
|
|
# The Databricks AI Gateway exposes one surface per protocol under the same
|
|
# workspace origin: Codex/OpenAI-Responses at ``/codex/v1`` and Anthropic
|
|
# Messages at ``/anthropic``. ``isaac configure codex`` writes the Codex
|
|
# base_url; pi-native rewrites it to the Anthropic surface Pi speaks natively.
|
|
_DATABRICKS_GATEWAY_CODEX_SUFFIX = "/codex/v1"
|
|
_DATABRICKS_GATEWAY_ANTHROPIC_SUFFIX = "/anthropic"
|
|
|
|
# Trusted parent domain suffixes for a Databricks-owned host. The AI Gateway
|
|
# lives under a per-workspace subdomain of one of these (the canonical form is
|
|
# ``<workspace>.ai-gateway.cloud.databricks.com``); the Azure / GCP control
|
|
# planes serve workspaces under their own parent domains. We anchor on the
|
|
# leading "." so a look-alike like ``...cloud.databricks.com.evil.test`` (which
|
|
# ends in ``.evil.test``) is rejected.
|
|
_DATABRICKS_TRUSTED_HOST_SUFFIXES = (
|
|
".cloud.databricks.com", # AWS workspaces + ai-gateway (incl. *.staging.cloud.databricks.com)
|
|
".azuredatabricks.net", # Azure Databricks
|
|
".gcp.databricks.com", # GCP Databricks
|
|
)
|
|
|
|
# A genuine AI Gateway host carries the ``ai-gateway`` DNS label; we require it
|
|
# (alongside a trusted suffix) so a non-gateway Databricks host isn't routed as
|
|
# the gateway's Anthropic surface.
|
|
_DATABRICKS_AI_GATEWAY_LABEL = "ai-gateway"
|
|
|
|
|
|
def _is_databricks_ai_gateway_url(base_url: str) -> bool:
|
|
"""Return ``True`` only for a genuine Databricks AI Gateway base URL.
|
|
|
|
Hardens the old substring scan over the whole base_url (scheme+host+path),
|
|
which look-alikes such as ``https://databricks-ai-gateway.evil.test/...``,
|
|
``https://x.cloud.databricks.com.evil.test/...`` or
|
|
``https://evil.test/databricks/ai-gateway/v1`` all defeated — leaking the
|
|
workspace bearer token to an attacker-controlled host. We parse the URL and
|
|
validate the *hostname* (not the raw string): require an ``https`` scheme, a
|
|
resolvable hostname carrying the ``ai-gateway`` DNS label, and a hostname
|
|
that ends with a trusted Databricks-owned parent domain suffix.
|
|
|
|
:param base_url: The codex provider table's ``base_url``.
|
|
:returns: ``True`` iff the URL is an https Databricks AI Gateway endpoint.
|
|
"""
|
|
parsed = urlparse(base_url)
|
|
if parsed.scheme != "https":
|
|
return False
|
|
hostname = parsed.hostname
|
|
if not hostname:
|
|
return False
|
|
hostname = hostname.lower()
|
|
# ``ai-gateway`` must be a full DNS label, not a substring of one (so
|
|
# ``databricks-ai-gateway.evil.test`` does not qualify on the label alone).
|
|
labels = hostname.split(".")
|
|
if _DATABRICKS_AI_GATEWAY_LABEL not in labels:
|
|
return False
|
|
return any(hostname.endswith(suffix) for suffix in _DATABRICKS_TRUSTED_HOST_SUFFIXES)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PiProviderConfig:
|
|
"""A resolved native-Pi provider, ready to render into ``models.json``.
|
|
|
|
:param provider_id: Provider id used in ``models.json`` and ``--provider``.
|
|
:param base_url: Endpoint base URL the ``pi`` CLI talks to.
|
|
:param api: Pi API type, e.g. ``"anthropic-messages"`` or
|
|
``"openai-responses"``.
|
|
:param model: Model id to select, e.g. ``"databricks-claude-sonnet-4-6"``.
|
|
:param api_key: Credential value for ``models.json`` ``apiKey`` — a literal
|
|
key, an env-var name, or a ``"!command"`` shell form (resolved by Pi at
|
|
request time, used for short-lived gateway tokens).
|
|
:param auth_header: When ``True``, Pi sends ``Authorization: Bearer
|
|
<apiKey>`` (gateways) instead of a provider-native key header.
|
|
"""
|
|
|
|
provider_id: str
|
|
base_url: str
|
|
api: str
|
|
model: str
|
|
api_key: str
|
|
auth_header: bool
|
|
|
|
def to_models_config(self) -> dict[str, Any]:
|
|
"""Render this provider as a Pi ``models.json`` mapping."""
|
|
provider: dict[str, Any] = {
|
|
"baseUrl": self.base_url,
|
|
"api": self.api,
|
|
"apiKey": self.api_key,
|
|
"models": [{"id": self.model}],
|
|
}
|
|
if self.auth_header:
|
|
provider["authHeader"] = True
|
|
return {"providers": {self.provider_id: provider}}
|
|
|
|
|
|
def _databricks_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiProviderConfig | None:
|
|
"""Resolve a Databricks-profile provider into Pi gateway config.
|
|
|
|
:param entry: The resolved default provider entry (``kind="databricks"``).
|
|
:param model: Session model override, or ``None`` to use the default.
|
|
:returns: The Pi provider config, or ``None`` when the profile's host
|
|
can't be resolved (caller falls back to Pi's own login).
|
|
"""
|
|
# Imported lazily: codex_executor pulls in heavy inner deps, and this
|
|
# module is imported on the runner's session-create path.
|
|
from omnigent.inner.codex_executor import _databricks_codex_auth_command
|
|
from omnigent.inner.databricks_executor import _read_databrickscfg_host
|
|
|
|
host = _read_databrickscfg_host(entry.profile)
|
|
if not host:
|
|
return None
|
|
host = host.rstrip("/")
|
|
auth_command = _databricks_codex_auth_command(host, entry.profile)
|
|
return PiProviderConfig(
|
|
provider_id=_PI_PROVIDER_ID,
|
|
base_url=f"{host}{_DATABRICKS_ANTHROPIC_GATEWAY_PATH}",
|
|
api="anthropic-messages",
|
|
model=model or _DATABRICKS_PI_DEFAULT_MODEL,
|
|
# Pi resolves a "!command" apiKey at request time, so the gateway
|
|
# bearer token is refreshed per request (the auth command itself
|
|
# force-refreshes), matching codex-native's refresh semantics.
|
|
api_key=f"!{auth_command}",
|
|
auth_header=True,
|
|
)
|
|
|
|
|
|
def _gateway_anthropic_base_url(codex_base_url: str) -> str:
|
|
"""Rewrite a Codex gateway base URL to the Anthropic Messages surface.
|
|
|
|
The Databricks AI Gateway serves each protocol under the same workspace
|
|
origin: ``.../codex/v1`` (OpenAI Responses) and ``.../anthropic``
|
|
(Anthropic Messages). ``isaac configure codex`` records the Codex URL;
|
|
Pi speaks Anthropic Messages natively, so we point it at ``/anthropic``.
|
|
|
|
:param codex_base_url: The provider table's ``base_url``, e.g.
|
|
``"https://<workspace>.ai-gateway.cloud.databricks.com/codex/v1"``.
|
|
:returns: The Anthropic-surface base URL, e.g.
|
|
``"https://<workspace>.ai-gateway.cloud.databricks.com/anthropic"``.
|
|
"""
|
|
trimmed = codex_base_url.rstrip("/")
|
|
if trimmed.endswith(_DATABRICKS_GATEWAY_CODEX_SUFFIX):
|
|
trimmed = trimmed[: -len(_DATABRICKS_GATEWAY_CODEX_SUFFIX)]
|
|
if trimmed.endswith(_DATABRICKS_GATEWAY_ANTHROPIC_SUFFIX):
|
|
return trimmed
|
|
return f"{trimmed}{_DATABRICKS_GATEWAY_ANTHROPIC_SUFFIX}"
|
|
|
|
|
|
def _cli_config_databricks_transport(entry: ProviderEntry) -> CodexConfigTransport | None:
|
|
"""Return the codex transport for a pi-consumable Databricks cli-config entry.
|
|
|
|
Shared core of :func:`_cli_config_pi_provider` and
|
|
:func:`cli_config_pi_provider_capable`: validates that *entry* is a codex
|
|
``cli-config`` whose pinned ``[model_providers.X]`` table in
|
|
``~/.codex/config.toml`` is a genuine Databricks AI Gateway carrying a
|
|
bearer-token command. Returns the resolved
|
|
:class:`~omnigent.onboarding.ambient.CodexConfigTransport` when so, else
|
|
``None`` (logging the reason at INFO).
|
|
|
|
:param entry: The provider entry (expected ``kind="cli-config"``).
|
|
:returns: The codex transport when *entry* is a pi-consumable Databricks
|
|
AI Gateway, else ``None``.
|
|
"""
|
|
# Only codex cli-config providers are model_provider-shaped today; a
|
|
# claude analog would be a different mechanism entirely.
|
|
if entry.cli != "codex" or not entry.model_provider:
|
|
return None
|
|
# Imported lazily: ambient pulls in onboarding-only deps, and this module
|
|
# is imported on the runner's session-create hot path.
|
|
from omnigent.onboarding.ambient import (
|
|
_codex_config_path,
|
|
codex_config_provider_transport,
|
|
)
|
|
|
|
transport = codex_config_provider_transport(_codex_config_path(), entry.model_provider)
|
|
if transport is None:
|
|
_LOGGER.info(
|
|
"pi-native: cli-config provider %r (model_provider %r) has no resolvable "
|
|
"[model_providers.%s] base_url in ~/.codex/config.toml; Pi will use its own login.",
|
|
entry.name,
|
|
entry.model_provider,
|
|
entry.model_provider,
|
|
)
|
|
return None
|
|
# Identify the Databricks AI Gateway robustly (not by workspace id): parse
|
|
# the codex base_url and validate its *hostname* against a trusted
|
|
# Databricks domain suffix allowlist plus the ``ai-gateway`` DNS label — a
|
|
# substring scan over the whole base_url would forward the workspace bearer
|
|
# token to look-alike hosts (e.g. ``databricks-ai-gateway.evil.test``).
|
|
if not _is_databricks_ai_gateway_url(transport.base_url):
|
|
_LOGGER.info(
|
|
"pi-native: cli-config provider %r (model_provider %r, base_url %r) is not a "
|
|
"recognized Databricks AI Gateway; Pi will use its own login.",
|
|
entry.name,
|
|
entry.model_provider,
|
|
transport.base_url,
|
|
)
|
|
return None
|
|
if not transport.auth_command:
|
|
_LOGGER.info(
|
|
"pi-native: Databricks cli-config provider %r carries no [model_providers.%s.auth] "
|
|
"token command; Pi will use its own login.",
|
|
entry.name,
|
|
entry.model_provider,
|
|
)
|
|
return None
|
|
return transport
|
|
|
|
|
|
def cli_config_pi_provider_capable(entry: ProviderEntry) -> bool:
|
|
"""Return whether a ``cli-config`` *entry* is pi-consumable.
|
|
|
|
A codex ``cli-config`` provider IS reusable by Pi exactly when
|
|
:func:`_cli_config_pi_provider` would resolve — i.e. its pinned
|
|
``[model_providers.X]`` table is a genuine Databricks AI Gateway with a
|
|
bearer-token command. This is the capability predicate the selection layer
|
|
(:mod:`omnigent.onboarding.provider_config`) consults to decide whether a
|
|
cli-config provider may serve / default the ``pi`` surface, keeping the
|
|
single source of truth here (and avoiding an import cycle —
|
|
``provider_config`` lazy-imports this rather than the reverse).
|
|
|
|
:param entry: The provider entry to classify (expected
|
|
``kind="cli-config"``; any other kind returns ``False``).
|
|
:returns: ``True`` iff Pi can route through this cli-config provider.
|
|
"""
|
|
return _cli_config_databricks_transport(entry) is not None
|
|
|
|
|
|
def _cli_config_pi_provider(entry: ProviderEntry, *, model: str | None) -> PiProviderConfig | None:
|
|
"""Resolve a Codex ``cli-config`` Databricks-gateway provider into Pi config.
|
|
|
|
The common enterprise setup: ``isaac configure codex`` writes a custom
|
|
``[model_providers.X]`` table (base_url + token-printing ``auth`` command)
|
|
into ``~/.codex/config.toml`` and ``omnigent setup`` adopts it as a
|
|
``cli-config`` provider. Codex-native routes through that table; pi-native
|
|
used to return ``None`` here — silently falling back to Pi's own
|
|
``/login`` (often stale creds) — which is the bug this fixes.
|
|
|
|
We read the *transport* (base URL + bearer-token command) from the codex
|
|
config table the entry pins, rewrite the base URL to the gateway's
|
|
Anthropic Messages surface (Pi speaks it natively), and emit a ``!command``
|
|
apiKey so Pi refreshes the gateway token per request — exactly like the
|
|
``databricks`` kind path. The workspace-specific base URL and token path
|
|
are read from config, never hardcoded.
|
|
|
|
:param entry: The resolved default provider (``kind="cli-config"``,
|
|
``cli="codex"``), carrying the ``model_provider`` id and display name.
|
|
:param model: Session model override, or ``None`` to use the default.
|
|
:returns: The Pi provider config, or ``None`` when the entry is not a
|
|
Databricks gateway, its codex provider table can't be resolved, or it
|
|
carries no token command (caller falls back to Pi's own login).
|
|
"""
|
|
transport = _cli_config_databricks_transport(entry)
|
|
if transport is None:
|
|
return None
|
|
return PiProviderConfig(
|
|
provider_id=_PI_PROVIDER_ID,
|
|
base_url=_gateway_anthropic_base_url(transport.base_url),
|
|
api="anthropic-messages",
|
|
model=model or _DATABRICKS_PI_DEFAULT_MODEL,
|
|
# Pi resolves a "!command" apiKey at request time, so the gateway
|
|
# bearer token (the codex auth command prints it) is refreshed per
|
|
# request — matching codex-native's refresh semantics.
|
|
api_key=f"!{transport.auth_command}",
|
|
auth_header=True,
|
|
)
|
|
|
|
|
|
def _inline_family_pi_provider(
|
|
entry: ProviderEntry, *, model: str | None
|
|
) -> PiProviderConfig | None:
|
|
"""Resolve a key/gateway/local provider into Pi config from its family.
|
|
|
|
Prefers the Anthropic family (Pi speaks ``anthropic-messages`` natively),
|
|
falling back to the OpenAI family via the Responses API.
|
|
|
|
:param entry: The resolved default provider entry.
|
|
:param model: Session model override, or ``None`` to use the family default.
|
|
:returns: The Pi provider config, or ``None`` when no usable family with a
|
|
base URL and credential is configured.
|
|
"""
|
|
for family_name in ("anthropic", "openai"):
|
|
family = entry.family(family_name)
|
|
if family is None or not family.base_url:
|
|
continue
|
|
# Determine the API type based on family and wire_api setting.
|
|
if family_name == "anthropic":
|
|
api = "anthropic-messages"
|
|
elif family.wire_api == CHAT_WIRE_API:
|
|
api = "openai-completions"
|
|
else:
|
|
api = "openai-responses"
|
|
# A static key (or $VAR) — Pi reads a literal/env apiKey directly; an
|
|
# auth_command becomes a "!command" Pi resolves at request time.
|
|
if family.api_key:
|
|
api_key = family.api_key
|
|
auth_header = False
|
|
elif family.auth_command:
|
|
api_key = f"!{family.auth_command}"
|
|
auth_header = True
|
|
else:
|
|
continue
|
|
resolved_model = model or entry.family_default_model(family_name)
|
|
if not resolved_model:
|
|
continue
|
|
# A session model override can arrive as a Databricks-gateway id
|
|
# (``databricks-claude-opus-4-7``) — that prefix only routes through the
|
|
# Databricks AI Gateway (``_databricks_pi_provider``). This family is
|
|
# vendor-direct (key / inline gateway / local Anthropic|OpenAI endpoint),
|
|
# so strip the mechanical ``databricks-`` prefix to the bare vendor id
|
|
# the endpoint can actually route. ``normalize_model_for_provider`` is
|
|
# prefix-mechanical: it only strips ``databricks-claude-*``/
|
|
# ``databricks-gpt-*`` and passes non-mechanical ids (e.g.
|
|
# ``zai-org/GLM-4.7``) and already-bare ids through unchanged. Family
|
|
# defaults are bare, so the no-override path is unaffected.
|
|
resolved_model = normalize_model_for_provider(resolved_model, KEY_KIND)
|
|
return PiProviderConfig(
|
|
provider_id=_PI_PROVIDER_ID,
|
|
base_url=family.base_url,
|
|
api=api,
|
|
model=resolved_model,
|
|
api_key=api_key,
|
|
auth_header=auth_header,
|
|
)
|
|
return None
|
|
|
|
|
|
def resolve_pi_native_provider(
|
|
*,
|
|
model: str | None = None,
|
|
config_loader: Callable[[], dict[str, Any]] = load_config,
|
|
) -> PiProviderConfig | None:
|
|
"""Resolve the omnigent-configured provider for a native Pi session.
|
|
|
|
Reads the default provider for the Pi surface from
|
|
``~/.omnigent/config.yaml`` and translates it into Pi ``models.json``
|
|
config. Returns ``None`` — leaving Pi to use its own ``/login`` — when no
|
|
usable provider is configured, or the default is a subscription / CLI-login
|
|
provider (a CLI's own login can't be reused outside that CLI).
|
|
|
|
:param model: Session model override (``model_override``), or ``None`` to
|
|
use the provider's default model.
|
|
:param config_loader: Injection seam for tests; defaults to
|
|
:func:`load_config`.
|
|
:returns: The resolved provider config, or ``None`` to fall back to Pi's
|
|
own credentials.
|
|
"""
|
|
try:
|
|
config = config_loader()
|
|
# Pi is multi-family; ``omnigent setup`` marks defaults per family, not
|
|
# for ``pi``. Use the shared house-pattern selection so pi resolves its
|
|
# default exactly like the rest of the codebase — an explicit pi default
|
|
# wins, else the anthropic (Pi's native surface) then openai family
|
|
# default, skipping kinds that can't drive pi. Crucially this now lets a
|
|
# cli-config Databricks AI Gateway through (it is pi-consumable via
|
|
# ``_cli_config_pi_provider``), so an unrelated anthropic-family default
|
|
# no longer shadows it.
|
|
entry = default_provider_for_harness(config, PI_SURFACE)
|
|
if entry is None:
|
|
_LOGGER.info(
|
|
"pi-native: no omnigent-configured provider for the pi/anthropic/openai "
|
|
"surface; Pi will use its own login."
|
|
)
|
|
return None
|
|
if entry.kind == DATABRICKS_KIND:
|
|
resolved = _databricks_pi_provider(entry, model=model)
|
|
elif entry.kind == CLI_CONFIG_KIND:
|
|
# A Codex cli-config provider whose [model_providers.X] table is the
|
|
# Databricks AI Gateway IS reusable by Pi (the gateway exposes an
|
|
# Anthropic surface Pi speaks). Translate it rather than dropping to
|
|
# Pi's own login — the bug this module fixes.
|
|
resolved = _cli_config_pi_provider(entry, model=model)
|
|
elif entry.kind in (KEY_KIND, GATEWAY_KIND, LOCAL_KIND):
|
|
resolved = _inline_family_pi_provider(entry, model=model)
|
|
else:
|
|
# subscription (a CLI's own login can't be reused outside that CLI):
|
|
# let Pi use its own login.
|
|
_LOGGER.info(
|
|
"pi-native: configured provider %r (kind %r) cannot drive Pi; "
|
|
"Pi will use its own login.",
|
|
entry.name,
|
|
entry.kind,
|
|
)
|
|
return None
|
|
if resolved is None:
|
|
# The provider matched a translatable kind but its details could not
|
|
# be resolved (e.g. a Databricks gateway whose codex config table is
|
|
# missing). Don't swallow it silently — a future user mystified by an
|
|
# "OpenRouter auth error despite configuring Databricks" needs this.
|
|
_LOGGER.warning(
|
|
"pi-native: configured provider %r (kind %r) could not be translated "
|
|
"into native Pi config; Pi will use its own login (which may hold "
|
|
"unrelated/stale credentials).",
|
|
entry.name,
|
|
entry.kind,
|
|
)
|
|
return resolved
|
|
except Exception: # noqa: BLE001 — any resolution failure must not break launch
|
|
# Any failure (malformed config, duplicate per-family default, or an
|
|
# unresolved ``api_key: $VAR``) falls back to Pi's own login rather than
|
|
# failing the terminal launch.
|
|
_LOGGER.warning(
|
|
"pi-native: failed to resolve the omnigent-configured provider; Pi will "
|
|
"use its own login.",
|
|
exc_info=True,
|
|
)
|
|
return None
|
|
|
|
|
|
def write_pi_models_config(agent_dir: Path, provider: PiProviderConfig) -> Path:
|
|
"""Write *provider* as ``models.json`` into a managed Pi config dir.
|
|
|
|
:param agent_dir: The managed Pi config dir (``PI_CODING_AGENT_DIR``).
|
|
:param provider: The resolved provider config to render.
|
|
:returns: Path to the written ``models.json``.
|
|
"""
|
|
agent_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
os.chmod(agent_dir, 0o700)
|
|
models_path = agent_dir / "models.json"
|
|
# 0o600: the apiKey may be a literal token (key-kind providers).
|
|
fd = os.open(models_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
json.dump(provider.to_models_config(), handle, indent=2, sort_keys=True)
|
|
handle.write("\n")
|
|
return models_path
|
|
|
|
|
|
def pi_native_provider_launch(
|
|
agent_dir: Path, provider: PiProviderConfig
|
|
) -> tuple[dict[str, str], list[str]]:
|
|
"""Write the managed config and return the launch env + CLI args for Pi.
|
|
|
|
:param agent_dir: The managed Pi config dir for this session.
|
|
:param provider: The resolved provider config.
|
|
:returns: ``(env, args)`` — the env vars to merge into the terminal spec
|
|
(relocating Pi's config dir) and the ``--provider``/``--model`` args to
|
|
append to the Pi command.
|
|
"""
|
|
write_pi_models_config(agent_dir, provider)
|
|
env = {PI_CODING_AGENT_DIR_ENV_VAR: str(agent_dir)}
|
|
args = ["--provider", provider.provider_id, "--model", provider.model]
|
|
return env, args
|