# Platforms and Platform-model Bindings

## Read Current State

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  "$GATEWAY_BASE_URL/api/admin/platforms"

curl --fail-with-body \
  -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  "$GATEWAY_BASE_URL/api/admin/models"
```

Platform responses expose masked `credentialsPreview`, not stored secrets.

## Reuse-compatible Platforms First

Before creating a platform or selecting `universal`:

1. Compare the upstream authentication, endpoint paths, content type, request schema, response schema, and sync/async lifecycle with the clients already represented by existing platform `config.specType` values.
2. If the upstream is compatible with an existing OpenAI, Gemini, or provider-specific client, keep that client type. For another compatible endpoint or account, the protocol layer normally needs only a different `baseUrl`; credentials and account-specific settings remain ordinary platform instance configuration.
3. Reuse the existing platform record only when it represents the same logical account/endpoint and changing it will not redirect unrelated models. Otherwise create another standard platform instance with the same compatible `specType`.
4. Use `universal` only when a documented request, authentication, task lifecycle, or response-mapping requirement cannot be expressed by a standard client.

Do not convert a compatible platform to `universal` merely because the host name, account, or provider-side model name differs.

## Create a Standard Platform

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "example-openai",
    "platformKey": "example-openai-primary",
    "name": "Example OpenAI Primary",
    "internalName": "example-openai-primary",
    "baseUrl": "https://api.example.com/v1",
    "authType": "bearer",
    "credentials": {"apiKey": "<provider-api-key>"},
    "config": {"specType": "openai"},
    "retryPolicy": {},
    "rateLimitPolicy": {},
    "defaultPricingMode": "inherit_discount",
    "defaultDiscountFactor": 1,
    "pricingRuleSetId": "",
    "priority": 100,
    "status": "enabled"
  }' \
  "$GATEWAY_BASE_URL/api/admin/platforms"
```

Use `PATCH /api/admin/platforms/{platformID}` with the complete platform body. Omit `credentials` to preserve existing secrets. A non-empty credentials object is merged into stored credentials. An empty object clears credentials and requires explicit confirmation.

OpenAI-compatible image edits use `multipart/form-data` by default. Only set `config.imageEditRequestFormat` to `json_url` when the upstream explicitly documents JSON URL input support; in that mode every image and mask must resolve to a public or presigned HTTP(S) URL. Omitting the setting, or setting it to `multipart`, keeps multipart file upload behavior.

## Upsert One Platform Model

`POST /api/admin/platforms/{platformID}/models` inserts or updates the `(platformID, modelName)` binding:

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "baseModelId": "<base-model-id>",
    "canonicalModelKey": "example-openai:example-chat",
    "modelName": "example-chat",
    "providerModelName": "example-chat",
    "modelAlias": "example-chat",
    "modelType": ["text_generate"],
    "displayName": "Example Chat",
    "capabilityOverride": {},
    "pricingMode": "inherit_discount",
    "discountFactor": 1,
    "pricingRuleSetId": "",
    "billingConfigOverride": {},
    "permissionConfig": {},
    "retryPolicy": {},
    "rateLimitPolicy": {},
    "runtimePolicySetId": "",
    "runtimePolicyOverride": {},
    "enabled": true
  }' \
  "$GATEWAY_BASE_URL/api/admin/platforms/<platform-id>/models"
```

When fields are omitted, defaults may be derived from the base model. For predictable updates, read the current binding and send the intended complete configuration.

### Override the real upstream invocation name

Use the base model as the stable system identity and `providerModelName` as the platform-specific name sent to the upstream API:

- `modelName` is the platform binding key and participates in the `(platformId, modelName)` upsert identity.
- `providerModelName` is the real model/deployment name used by the selected runtime client.
- Bind the same `baseModelId` on multiple platforms and set a different `providerModelName` on each binding when providers expose different invocation names.
- Do not create duplicate base models solely for aliases such as a vendor deployment ID, endpoint ID, dated model ID, or regional model name.

Example:

```json
[
  {
    "platform": "platform-a",
    "baseModelId": "base-example-chat",
    "modelName": "example-chat",
    "providerModelName": "example-chat-2026-07"
  },
  {
    "platform": "platform-b",
    "baseModelId": "base-example-chat",
    "modelName": "example-chat",
    "providerModelName": "deployment-prod-42"
  }
]
```

The current create/upsert implementation stores the platform-model binding as enabled even when the request contains `"enabled": false`. Do not rely on that input field for canary isolation. Use an isolated test deployment or keep the whole platform disabled until configuration review is complete; enable the platform only for an approved protocol test.

## Full Replacement and Deletion

`PUT /api/admin/platforms/{platformID}/models` reconciles the platform to exactly the supplied `models` list. Omitted bindings are deleted together with their platform-model access rules. Never use it for a single-model update.

Delete one binding with `DELETE /api/admin/platform-models/{modelID}` after confirmation. Delete a platform with `DELETE /api/admin/platforms/{platformID}` only after reviewing all bindings and access-rule impact.
