feat: merge opus comfyui and login updates
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# route-candidate-api Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-route-rebuild-pipeline. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: `GET /api/models/token-candidates` is registered
|
||||
|
||||
The admin router SHALL register `GET /api/models/token-candidates`
|
||||
behind the standard admin auth layer. The handler SHALL return a JSON
|
||||
object with exactly four top-level keys: `models`,
|
||||
`modelsWithoutToken`, `modelsMissingTokenGroups`, `endpointTypesByModel`.
|
||||
|
||||
#### Scenario: Endpoint returns 200 with the four-key shape
|
||||
- **WHEN** an authenticated admin sends `GET /api/models/token-candidates`
|
||||
- **THEN** the response is HTTP 200 with a JSON body containing keys
|
||||
`models`, `modelsWithoutToken`, `modelsMissingTokenGroups`, and
|
||||
`endpointTypesByModel` — no extra top-level keys, no missing keys
|
||||
|
||||
#### Scenario: Unauthenticated request is rejected
|
||||
- **WHEN** a request without a valid admin token reaches the endpoint
|
||||
- **THEN** the admin auth layer returns 401 before the handler runs
|
||||
|
||||
### Requirement: `models` lists every (model, token) candidate
|
||||
|
||||
The `models` field SHALL be an object whose keys are model names and
|
||||
whose values are arrays of candidate descriptors. A `(model, token)`
|
||||
combination SHALL appear in the `models` array for that model exactly
|
||||
when ALL of the following hold:
|
||||
- a row in `model_availability` reports `available = 1` for the model
|
||||
on the token's account
|
||||
- the token's `enabled` is truthy (NULL treated as enabled)
|
||||
- the token's `value_status` is `'ready'` (NULL treated as ready)
|
||||
- the account's `status = 'active'`
|
||||
- the site's `status` is `'active'` (NULL treated as active)
|
||||
|
||||
Each candidate descriptor SHALL contain: `accountId`, `tokenId`,
|
||||
`tokenName`, `isDefault` (boolean), `username`, `siteId`, `siteName`.
|
||||
Within a single model's array, descriptors SHALL be deduplicated by
|
||||
`tokenId`.
|
||||
|
||||
#### Scenario: Active token with available model appears
|
||||
- **WHEN** account 5 (active, site active) has token 11 (enabled,
|
||||
ready) and `model_availability(account=5, model='gpt-4')` is
|
||||
available
|
||||
- **THEN** `models['gpt-4']` includes a descriptor with
|
||||
`accountId = 5, tokenId = 11`
|
||||
|
||||
#### Scenario: Disabled token is excluded
|
||||
- **WHEN** account 5 has token 11 with `enabled = 0`
|
||||
- **THEN** no descriptor with `tokenId = 11` appears in `models` for
|
||||
any model
|
||||
|
||||
#### Scenario: Duplicate tokens in source data deduped
|
||||
- **WHEN** the underlying join produces two rows for the same
|
||||
`(model, tokenId)` pair (e.g., joins fan out)
|
||||
- **THEN** the descriptor appears exactly once in
|
||||
`models[model_name]`
|
||||
|
||||
### Requirement: `modelsWithoutToken` surfaces accounts missing tokens
|
||||
|
||||
The `modelsWithoutToken` field SHALL be an object keyed by model name.
|
||||
For each `(account, model)` combination where the model is available
|
||||
on the active account but no qualifying token (per the `models`
|
||||
requirement) exists for that account, the handler SHALL include a
|
||||
descriptor `{ accountId, username, siteId, siteName }` under that
|
||||
model name. Descriptors SHALL be deduplicated by `accountId` within
|
||||
each model's array.
|
||||
|
||||
#### Scenario: Account with available model but no enabled tokens
|
||||
- **WHEN** account 8 reports `gpt-4` available but has zero rows in
|
||||
`account_tokens` (or all rows are disabled)
|
||||
- **THEN** `modelsWithoutToken['gpt-4']` includes a descriptor with
|
||||
`accountId = 8`, and that account does NOT appear in `models['gpt-4']`
|
||||
|
||||
#### Scenario: Account with token does not appear here
|
||||
- **WHEN** account 5 reports `gpt-4` available and has a usable token
|
||||
- **THEN** account 5 does NOT appear in `modelsWithoutToken['gpt-4']`
|
||||
|
||||
### Requirement: `modelsMissingTokenGroups` and `endpointTypesByModel` are empty
|
||||
|
||||
The handler SHALL return `modelsMissingTokenGroups` and
|
||||
`endpointTypesByModel` as empty JSON objects (`{}`). The frontend
|
||||
contract permits empty objects and these fields SHALL NOT be
|
||||
populated until a marketplace-cache or pricing-catalog source is
|
||||
added to the backend.
|
||||
|
||||
#### Scenario: Both fields are empty objects
|
||||
- **WHEN** the handler returns successfully under any state of the
|
||||
database
|
||||
- **THEN** `response.modelsMissingTokenGroups === {}` and
|
||||
`response.endpointTypesByModel === {}`
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# route-rebuild-pipeline Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-route-rebuild-pipeline. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Route rebuild reads from `model_availability`
|
||||
|
||||
The rebuild pipeline SHALL derive its working set from the
|
||||
`model_availability` table joined to `accounts`, filtering out:
|
||||
- accounts whose `status` is not `'active'`
|
||||
- rows whose `available` is not `1` (treating `NULL` as not available)
|
||||
- `(account, model)` combinations covered by `site_disabled_models`
|
||||
for the account's `site_id`
|
||||
|
||||
The pipeline SHALL NOT make outbound HTTP requests to upstream platforms
|
||||
during a rebuild. Refreshing `model_availability` is the responsibility
|
||||
of the existing `POST /api/accounts/{id}/models/refresh` endpoint and
|
||||
is out of scope for this requirement.
|
||||
|
||||
#### Scenario: Inactive account is excluded
|
||||
- **WHEN** an account with `status = 'suspended'` has rows in
|
||||
`model_availability`
|
||||
- **THEN** no `token_routes` row is created on its behalf and no
|
||||
`route_channels` row references its `account_id`
|
||||
|
||||
#### Scenario: Site-disabled model is excluded
|
||||
- **WHEN** `site_disabled_models` contains `(site_id = 7, model_name = 'gpt-x')`
|
||||
and an active account on site 7 has `model_availability` for `gpt-x`
|
||||
- **THEN** the rebuild does not include that account in the channel set
|
||||
for the `gpt-x` route
|
||||
|
||||
### Requirement: Rebuild aggregates one route per distinct model name
|
||||
|
||||
The rebuild SHALL ensure exactly one row exists in `token_routes` for
|
||||
every distinct `model_name` in the filtered working set, keyed by
|
||||
`model_pattern = model_name`. The rebuild SHALL reuse existing rows
|
||||
when their `model_pattern` already matches; new rows SHALL be inserted
|
||||
with `routing_strategy = 'weighted'`, `route_mode = 'pattern'`, and
|
||||
`enabled = 1`.
|
||||
|
||||
The rebuild MUST NOT overwrite `display_name`, `display_icon`, or any
|
||||
other fields on existing `token_routes` rows. Operators rely on these
|
||||
fields being editable from the frontend without being clobbered on
|
||||
re-runs.
|
||||
|
||||
#### Scenario: New model creates a new route
|
||||
- **WHEN** model `claude-4-opus` appears in the working set and no
|
||||
`token_routes` row has `model_pattern = 'claude-4-opus'`
|
||||
- **THEN** the rebuild inserts a `token_routes` row with
|
||||
`model_pattern = 'claude-4-opus'`, `routing_strategy = 'weighted'`,
|
||||
`route_mode = 'pattern'`, `enabled = 1`
|
||||
|
||||
#### Scenario: Existing route is reused without overwrite
|
||||
- **WHEN** a `token_routes` row exists with
|
||||
`model_pattern = 'gpt-4'` and `display_name = 'GPT-4 Turbo (Custom)'`
|
||||
- **THEN** the rebuild leaves `display_name` unchanged and reuses the
|
||||
same `id` for channel reconciliation
|
||||
|
||||
### Requirement: Channel reconciliation preserves manual overrides
|
||||
|
||||
For each route, the rebuild SHALL reconcile `route_channels` rows
|
||||
keyed by `(account_id, source_model)` against the desired set derived
|
||||
from `model_availability`. Rows with `manual_override = 1` SHALL be
|
||||
treated as immutable: not inserted, not updated, not deleted. Rows
|
||||
with `manual_override = 0` (or `NULL`) SHALL be:
|
||||
- inserted when missing from the existing set
|
||||
- hard-deleted when no longer in the desired set
|
||||
|
||||
The desired set SHALL exclude any `(account, model)` combination
|
||||
already covered by a `manual_override = 1` row, so the rebuild never
|
||||
inserts an auto channel that shadows a manual one.
|
||||
|
||||
#### Scenario: Manual channel survives rebuild
|
||||
- **WHEN** a `route_channels` row has `manual_override = 1` and its
|
||||
`(account_id, source_model)` is no longer in the desired set
|
||||
- **THEN** the row is left untouched and counted in
|
||||
`RebuildSummary.skipped_manual_channels`
|
||||
|
||||
#### Scenario: Stale auto channel is deleted
|
||||
- **WHEN** a `route_channels` row has `manual_override = 0`,
|
||||
`account_id = 12`, `source_model = 'old-model'`, and no longer
|
||||
appears in the desired set
|
||||
- **THEN** the row is deleted and counted in
|
||||
`RebuildSummary.deleted_channels`
|
||||
|
||||
#### Scenario: New auto channel is inserted with default token
|
||||
- **WHEN** account 12 newly reports `gpt-5` available and has at least
|
||||
one enabled `account_tokens` row
|
||||
- **THEN** a new `route_channels` row is inserted with `route_id`
|
||||
pointing at the `gpt-5` route, `account_id = 12`, the
|
||||
default-flagged enabled token's id (or the lowest id when none is
|
||||
default), `priority = 0`, `weight = 10`, `enabled = 1`,
|
||||
`manual_override = 0`
|
||||
|
||||
#### Scenario: Account without tokens still gets a channel
|
||||
- **WHEN** account 13 reports `gpt-5` available but has no enabled
|
||||
`account_tokens` rows
|
||||
- **THEN** a `route_channels` row is inserted with `token_id = NULL`,
|
||||
relying on the proxy layer's account-level token fallback
|
||||
|
||||
### Requirement: Rebuild runs in a single transaction
|
||||
|
||||
The rebuild SHALL execute every read and write inside one database
|
||||
transaction, committed at the end. A partial failure MUST NOT leave
|
||||
`token_routes` and `route_channels` mutually inconsistent.
|
||||
|
||||
#### Scenario: Failure mid-rebuild rolls back
|
||||
- **WHEN** an `INSERT` into `route_channels` fails partway through
|
||||
the rebuild
|
||||
- **THEN** the transaction aborts and no `token_routes` insert from
|
||||
the same rebuild persists
|
||||
|
||||
### Requirement: Rebuild summary matches frontend contract
|
||||
|
||||
The rebuild SHALL return a `RebuildSummary` containing the fields
|
||||
`createdRoutes`, `reusedRoutes`, `createdChannels`, `deletedChannels`,
|
||||
`skippedManualChannels`, `scannedModels`, `scannedAccounts` (camelCase
|
||||
when serialized). The `POST /api/routes/rebuild` handler SHALL embed
|
||||
this object under the JSON key `rebuild`, alongside `success: true`.
|
||||
|
||||
`TokenRoutes.tsx:411-412` reads `rebuild.createdRoutes` and
|
||||
`rebuild.createdChannels`; both MUST be present and SHALL count only
|
||||
rows actually inserted by the current rebuild (not pre-existing rows
|
||||
that were reused).
|
||||
|
||||
#### Scenario: Rebuild response shape
|
||||
- **WHEN** the handler completes successfully
|
||||
- **THEN** the response body is
|
||||
`{ "success": true, "rebuild": { "createdRoutes": <n1>,
|
||||
"reusedRoutes": <n2>, "createdChannels": <n3>,
|
||||
"deletedChannels": <n4>, "skippedManualChannels": <n5>,
|
||||
"scannedModels": <n6>, "scannedAccounts": <n7> } }`
|
||||
|
||||
#### Scenario: Reused route does not increment createdRoutes
|
||||
- **WHEN** every model in the working set has a pre-existing
|
||||
`token_routes` row
|
||||
- **THEN** `createdRoutes = 0` and `reusedRoutes` equals the number of
|
||||
distinct models scanned
|
||||
|
||||
### Requirement: Auto-rebuild after account creation
|
||||
|
||||
The system SHALL fire one route rebuild in the background after
|
||||
`POST /api/accounts` returns success for at least one created
|
||||
account. The HTTP response SHALL return without waiting for the
|
||||
rebuild to finish. Errors from the background rebuild SHALL be logged
|
||||
at `warn` level and SHALL NOT affect the response status or body.
|
||||
|
||||
#### Scenario: Single-account create triggers rebuild
|
||||
- **WHEN** `POST /api/accounts` creates exactly one account
|
||||
- **THEN** `spawn_route_rebuild` is invoked once with the cloned
|
||||
`AppState` and the response returns immediately
|
||||
|
||||
#### Scenario: Batch-create with all failures does not trigger
|
||||
- **WHEN** `POST /api/accounts` returns with `created.len() == 0` and
|
||||
`failed.len() > 0`
|
||||
- **THEN** `spawn_route_rebuild` is not invoked
|
||||
|
||||
Reference in New Issue
Block a user