Add model provider discovery to the app server (#58845)

## What changed

- Add `modelProvider/list` to return OpenAI and explicitly configured providers, provider-specific settings, and the selected default provider.
- Report `canEdit` and `canChangeDefault` based on configuration requirements, layer precedence, and whether user configuration is enabled.
- Describe gateway authentication without resolving credentials, and omit endpoint URLs that are unsuitable for display.
- Document the API and update generated JSON schemas, TypeScript types, and Python models.

## Testing

Add app-server tests for provider discovery, authentication modes, unsafe URL suppression, and configuration editability, plus a Python wire-format round-trip test.

GitOrigin-RevId: 8ba26637cd6bdd075e0e815e88aeeb04c53a2364
This commit is contained in:
alexsong-oai
2026-11-02 08:31:14 +00:00
committed by copyberry
parent 05c08d7655
commit 3026a8e54e
29 changed files with 1763 additions and 6 deletions
@@ -1800,6 +1800,37 @@ class FuzzyFileSearchSessionUpdatedNotification(BaseModel):
session_id: Annotated[str, Field(alias="sessionId")]
class NoneGatewayAuth(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
mode: Annotated[Literal["none"], Field(title="NoneGatewayAuthMode")]
class EnvironmentVariableGatewayAuth(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
env_key: Annotated[str, Field(alias="envKey")]
mode: Annotated[
Literal["environmentVariable"], Field(title="EnvironmentVariableGatewayAuthMode")
]
class ExternallyConfiguredGatewayAuth(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
env_key: Annotated[str | None, Field(alias="envKey")] = None
mode: Annotated[Literal["externallyConfigured"], Field(title="ModeGatewayAuthMode")]
class GatewayAuth(
RootModel[NoneGatewayAuth | EnvironmentVariableGatewayAuth | ExternallyConfiguredGatewayAuth]
):
root: NoneGatewayAuth | EnvironmentVariableGatewayAuth | ExternallyConfiguredGatewayAuth
class GatewayOAuthCancelResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -2854,6 +2885,12 @@ class ModelProviderError(
]
class ModelProviderListParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
class ModelRerouteReason(RootModel[Literal["highRiskCyberActivity"]]):
root: Literal["highRiskCyberActivity"]
@@ -3686,6 +3723,62 @@ class ProjectSortKey(Enum):
recency_at = "recencyAt"
class OpenaiProviderSettings(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
type: Annotated[Literal["openai"], Field(title="OpenaiProviderSettingsType")]
class CustomGatewayProviderSettings(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
auth: GatewayAuth
type: Annotated[Literal["customGateway"], Field(title="CustomGatewayProviderSettingsType")]
class BedrockProviderSettings(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
profile: str | None = None
region: str | None = None
type: Annotated[Literal["bedrock"], Field(title="BedrockProviderSettingsType")]
class GcpProviderSettings(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
credential_file: Annotated[
str | None,
Field(
alias="credentialFile",
description="Path on the app-server host, without credential-file contents.",
),
] = None
location: str | None = None
project_id: Annotated[str | None, Field(alias="projectId")] = None
type: Annotated[Literal["gcp"], Field(title="GcpProviderSettingsType")]
class ProviderSettings(
RootModel[
OpenaiProviderSettings
| CustomGatewayProviderSettings
| BedrockProviderSettings
| GcpProviderSettings
]
):
root: (
OpenaiProviderSettings
| CustomGatewayProviderSettings
| BedrockProviderSettings
| GcpProviderSettings
)
class RateLimitReachedType(Enum):
rate_limit_reached = "rate_limit_reached"
workspace_owner_credits_depleted = "workspace_owner_credits_depleted"
@@ -7800,6 +7893,15 @@ class AccountGatewayOAuthCancelRequest(BaseModel):
params: Annotated[None, Field(None)] = None
class ModelProviderListRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: RequestId
method: Annotated[Literal["modelProvider/list"], Field(title="ModelProvider/listRequestMethod")]
params: ModelProviderListParams
class ModelProviderCapabilitiesReadRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -9234,6 +9336,29 @@ class ModelListResponse(BaseModel):
] = None
class ModelProviderSummary(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
base_url: Annotated[
str | None,
Field(
alias="baseUrl",
description="Null when the endpoint is absent or cannot be safely displayed.",
),
] = None
can_edit: Annotated[
bool,
Field(
alias="canEdit",
description="Whether a supported configuration field can be edited in the active user layer.",
),
]
id: str
name: str
settings: ProviderSettings
class NewThreadModelDefaults(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -11649,6 +11774,15 @@ class ListMcpServerStatusResponse(BaseModel):
] = None
class ModelProviderListResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
can_change_default: Annotated[bool, Field(alias="canChangeDefault")]
data: list[ModelProviderSummary]
default_provider_id: Annotated[str, Field(alias="defaultProviderId")]
class ModelsRequirements(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -13354,6 +13488,7 @@ class ClientRequest(
| AccountGatewayOAuthReadRequest
| AccountGatewayOAuthLoginRequest
| AccountGatewayOAuthCancelRequest
| ModelProviderListRequest
| ModelProviderCapabilitiesReadRequest
| ExperimentalFeatureListRequest
| PermissionProfileListRequest
@@ -13463,6 +13598,7 @@ class ClientRequest(
| AccountGatewayOAuthReadRequest
| AccountGatewayOAuthLoginRequest
| AccountGatewayOAuthCancelRequest
| ModelProviderListRequest
| ModelProviderCapabilitiesReadRequest
| ExperimentalFeatureListRequest
| PermissionProfileListRequest
@@ -20,6 +20,7 @@ from openai_codex.generated.v2_all import (
AuthRecoveryNotification,
CommandGuardianApprovalReviewAction,
GetAccountResponse,
ModelProviderListResponse,
PlanType,
ReasoningEffort,
ReasoningEffortOption,
@@ -41,6 +42,57 @@ from openai_codex.types import ThreadSource
ROOT = Path(__file__).resolve().parents[1]
def test_model_provider_list_preserves_wire_fields_for_each_auth_mode() -> None:
wire = {
"data": [
{
"id": "openai",
"name": "OpenAI",
"canEdit": False,
"baseUrl": None,
"settings": {"type": "openai"},
},
{
"id": "amazon-bedrock",
"name": "Amazon Bedrock Mantle",
"canEdit": True,
"baseUrl": None,
"settings": {"type": "bedrock", "profile": "work", "region": "us-east-1"},
},
{
"id": "gcp",
"name": "Google Cloud Vertex AI",
"canEdit": True,
"baseUrl": None,
"settings": {
"type": "gcp",
"projectId": "test-project",
"location": "global",
"credentialFile": "/host/adc.json",
},
},
*[
{
"id": auth["mode"],
"name": "Gateway",
"canEdit": True,
"baseUrl": "https://gateway.example.test",
"settings": {"type": "customGateway", "auth": auth},
}
for auth in [
{"mode": "none"},
{"mode": "environmentVariable", "envKey": "GATEWAY_KEY"},
{"mode": "externallyConfigured", "envKey": "EXTERNAL_KEY"},
]
],
],
"defaultProviderId": "gcp",
"canChangeDefault": True,
}
response = ModelProviderListResponse.model_validate(wire)
assert response.model_dump(mode="json", by_alias=True) == wire
def test_inline_review_delivery_round_trips_as_an_enum() -> None:
wire = {"threadId": "thread-1", "target": {"type": "uncommittedChanges"}, "delivery": "inline"}
params = ReviewStartParams.model_validate(wire)