From fb1eb732f0ebcdb9217ea5871fc5bf0337e8747e Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Thu, 22 Oct 2026 14:27:59 -0700 Subject: [PATCH 1/2] contracts: carve GET /v1/models down to the default form the CLI reads InferenceInfra's models list is about to answer a second form, `?format=openrouter-v2`: the operation gains a `format` query parameter and its 200 becomes `ModelListResponse`, a `oneOf` of `ModelList` and `OpenRouterModelDocumentList` with no discriminator (the query picks the variant). The CLI sends no `format`, so it only ever reads `ModelList`. Left in the slice, the second form stops the binding generator ("unsupported schema shape" at OpenRouterV2ParameterDescriptor.properties: an inline object that refers to itself), and any later change to that form would mark the CLI's pin stale for something it never reads. extract-cli-contract.py now leaves the `format` parameter out of listModels and reads `ModelListResponse` as `ModelList` (CLI_NARROWED_OPERATIONS). A source contract without them is carried unchanged, so this commit alone moves no pin: wally-cli-v1.openapi.json is byte-identical. tests/test_contract_sync.py gains three cases, red before the change: the slice keeps only the default form; a change to the other form does not move the slice; an older source is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- contracts/extract-cli-contract.py | 46 ++++++++++++++++++ tests/test_contract_sync.py | 77 ++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/contracts/extract-cli-contract.py b/contracts/extract-cli-contract.py index ae99d7ff..b45de19d 100644 --- a/contracts/extract-cli-contract.py +++ b/contracts/extract-cli-contract.py @@ -50,6 +50,21 @@ # export behind `wally account usage --requests`. "listCliUsageRequests", } +# Operations whose source description also carries a form the CLI never asks for. The CLI sends +# `GET /v1/models` with no `format` query, so it only ever reads the default `ModelList`; the +# `?format=openrouter-v2` document is a second 200 shape the control plane serves to other clients +# (a `oneOf` with no discriminator, because the query picks the variant, not a field). It is cut out +# of the slice rather than generated: the slice is what the CLI consumes, the binding generator reads +# closed, discriminated shapes only, and a change to the other form must not flag the CLI as stale. +# `parameters` names component parameters to leave out; `schemas` maps a component schema the +# operation references to the one the CLI reads instead. An operation that does not reference them +# (an older source contract) is carried unchanged. +CLI_NARROWED_OPERATIONS = { + "listModels": { + "parameters": {"ModelListFormatQuery"}, + "schemas": {"ModelListResponse": "ModelList"}, + }, +} HTTP_METHODS = {"get", "post", "put", "delete", "patch"} # The component sections a kept operation may reference. Until wally #81 only # `schemas` was carried, so every `#/components/{parameters,responses}/...` @@ -78,6 +93,30 @@ def _refs(value: object, out: set[str]) -> None: _refs(child, out) +def _narrow(value: object, parameters: set[str], schemas: dict[str, str]) -> object: + """`value` with the narrowing of CLI_NARROWED_OPERATIONS applied; a copy, never the source.""" + if isinstance(value, dict): + ref = value.get("$ref") + if isinstance(ref, str): + target = ref.rsplit("/", 1)[-1] + if ref.startswith("#/components/schemas/") and target in schemas: + return {**value, "$ref": f"#/components/schemas/{schemas[target]}"} + return {key: _narrow(child, parameters, schemas) for key, child in value.items()} + if isinstance(value, list): + kept = [ + child + for child in value + if not ( + isinstance(child, dict) + and isinstance(child.get("$ref"), str) + and child["$ref"].startswith("#/components/parameters/") + and child["$ref"].rsplit("/", 1)[-1] in parameters + ) + ] + return [_narrow(child, parameters, schemas) for child in kept] + return value + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("source", type=Path, help="path to control-plane-v1.openapi.json") @@ -129,6 +168,13 @@ def visit(key: str) -> None: kept = {} for method, operation in item.items(): if method in HTTP_METHODS and operation.get("operationId") in CLI_OPERATION_IDS: + narrowing = CLI_NARROWED_OPERATIONS.get(operation["operationId"]) + if narrowing: + had_parameters = bool(operation.get("parameters")) + operation = _narrow(operation, narrowing["parameters"], narrowing["schemas"]) + if had_parameters and not operation["parameters"]: + # Every parameter was one the CLI does not send: no empty list stays behind. + del operation["parameters"] kept[method] = operation found: set[str] = set() _refs(operation, found) diff --git a/tests/test_contract_sync.py b/tests/test_contract_sync.py index f4090787..2b1c2192 100644 --- a/tests/test_contract_sync.py +++ b/tests/test_contract_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""The CLI contract sync wrapper describes itself and refuses a pin with no provenance.""" +"""The CLI contract sync wrapper describes itself and refuses a pin with no provenance; the extractor carves the slice.""" from __future__ import annotations @@ -13,6 +13,7 @@ REPO = Path(__file__).resolve().parents[1] SYNC = REPO / "contracts" / "sync_from_inferenceinfra.py" EXTRACT = REPO / "contracts" / "wally-cli-v1.openapi.json" +EXTRACTOR = REPO / "contracts" / "extract-cli-contract.py" def _check(*extra: str) -> subprocess.CompletedProcess[str]: @@ -102,5 +103,79 @@ def test_check_refuses_each_missing_provenance_field(self): self._assert_refused(document, field) +class ExtractorNarrowingTests(unittest.TestCase): + """`GET /v1/models` is carved to the one form the CLI reads (extract-cli-contract.py).""" + + def _extract(self, source: dict) -> dict: + with tempfile.TemporaryDirectory() as directory: + source_path = Path(directory) / "control-plane-v1.openapi.json" + source_path.write_text(json.dumps(source), encoding="utf-8") + output = Path(directory) / "extract.json" + result = subprocess.run( + [sys.executable, str(EXTRACTOR), str(source_path), "--output", str(output)], + cwd=REPO, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(0, result.returncode, result.stderr + result.stdout) + return json.loads(output.read_text(encoding="utf-8")) + + def _source_with_a_format_query(self) -> dict: + """The committed extract, with listModels as the control plane describes it once the + models list can also answer `?format=openrouter-v2`: a format query, and a `oneOf` + response with no discriminator whose second branch is a shape the generator cannot read.""" + document = json.loads(EXTRACT.read_text(encoding="utf-8")) + components = document["components"] + components["parameters"]["ModelListFormatQuery"] = { + "in": "query", + "name": "format", + "required": False, + "schema": {"enum": ["openrouter-v2"], "type": "string"}, + } + components["schemas"]["OpenRouterModelDocumentList"] = { + "type": "object", + "properties": {"data": {"type": "array", "items": {"type": "object"}}}, + } + components["schemas"]["ModelListResponse"] = { + "oneOf": [ + {"$ref": "#/components/schemas/ModelList"}, + {"$ref": "#/components/schemas/OpenRouterModelDocumentList"}, + ] + } + operation = document["paths"]["/v1/models"]["get"] + operation["parameters"] = [{"$ref": "#/components/parameters/ModelListFormatQuery"}] + operation["responses"]["200"]["content"]["application/json"]["schema"] = { + "$ref": "#/components/schemas/ModelListResponse" + } + return document + + def test_models_list_keeps_only_the_default_form(self): + extract = self._extract(self._source_with_a_format_query()) + operation = extract["paths"]["/v1/models"]["get"] + self.assertEqual( + {"$ref": "#/components/schemas/ModelList"}, + operation["responses"]["200"]["content"]["application/json"]["schema"], + ) + self.assertNotIn("parameters", operation) + self.assertNotIn("ModelListFormatQuery", extract["components"]["parameters"]) + self.assertNotIn("ModelListResponse", extract["components"]["schemas"]) + self.assertNotIn("OpenRouterModelDocumentList", extract["components"]["schemas"]) + + def test_the_other_form_changing_does_not_move_the_slice(self): + before = self._source_with_a_format_query() + after = self._source_with_a_format_query() + after["components"]["schemas"]["OpenRouterModelDocumentList"]["properties"]["next"] = { + "type": "string" + } + self.assertEqual(self._extract(before), self._extract(after)) + + def test_a_source_without_the_format_query_is_carried_unchanged(self): + document = json.loads(EXTRACT.read_text(encoding="utf-8")) + self.assertEqual( + document["paths"], self._extract(document)["paths"] + ) + + if __name__ == "__main__": unittest.main() From 34d54f6e0badd4a0b9f2174cd5e1dffa3647fa7c Mon Sep 17 00:00:00 2001 From: Sanchit Monga Date: Thu, 22 Oct 2026 14:28:11 -0700 Subject: [PATCH 2/2] contracts: pin the CLI contract from InferenceInfra's code-complete line (a8259392) Wally main's pin is the live line's (3fb1fd00, `fix/codex-harness-live`). InferenceInfra's `new_infra` -> `staging` promotion compares this repo's main pin to the control-plane document at its head ("downstream contract freshness / Wally main vs this control-plane") and stays red until this pin matches. This re-syncs it with `contracts/sync_from_inferenceinfra.py --from `. Source: InferenceInfra track/api-parity a8259392, which is `integration/code-complete` (c76e9697) plus the catalogue and public models-list change. Its control-plane-v1 document is byte-identical to the merge of that track into the integration branch, which is the document that will be promoted. What moves in the CLI slice (additive; nothing the CLI reads is removed): - ApiErrorCode gains 34 values the staging line added (organisations, invitations, partners, operator, price-book errors). - `GET /v1/models` is anonymous-capable (`security: [BearerAuth, {}]`), declares the `x-request-id` response header, and has new summary/description text. - Wording of the reasoning, datacenter and whoami descriptions follows the source. - The `format=openrouter-v2` form of the models list is NOT in the slice (previous commit). console_contract.rs is regenerated (CONTRACT_SHA256 moves). The only use of the error enum outside it is `== ApiErrorCode::KCardRequired` in console.rs, so no match needs new arms. The decisions contract is not touched: Wally main already carries the Eve format 4 revision (ecd738b2), which is byte-identical to the one in track/api-parity. Checked: - python3 tests/test_contract_sync.py 8 tests OK - python3 contracts/generate_console_binding.py --check both bindings match their pins - python3 contracts/sync_from_inferenceinfra.py --check Wally CLI lock OK (a8259392) - the regenerated console_contract.rs compiles on its own (cargo check, serde_json only) - InferenceInfra's own gate, `check_consumer_impact.py --against-wally-pin`, run on a track/api-parity checkout against this tree: "consumer-impact: none" (CLI slice and decisions). A full `cargo check` of wally could not run here (build.rs needs the CMake kit); CI compiles it. Merge order: this lands before InferenceInfra's promotion PR (its consumer-impact gate reads this repo's main). If the control-plane contract moves again before then, re-run the sync. Co-Authored-By: Claude Sonnet 5.5 --- contracts/wally-cli-v1.openapi.json | 83 ++++++++++++++++++--- src/account/console_contract.rs | 108 +++++++++++++++++++++++++++- 2 files changed, 179 insertions(+), 12 deletions(-) diff --git a/contracts/wally-cli-v1.openapi.json b/contracts/wally-cli-v1.openapi.json index 0bf5d948..9ff0d76b 100644 --- a/contracts/wally-cli-v1.openapi.json +++ b/contracts/wally-cli-v1.openapi.json @@ -329,7 +329,41 @@ "gateway_operation_pending", "reconciliation_unverified", "retention_managed_by_runanywhere", - "card_required" + "card_required", + "invalid_account_id", + "account_not_found", + "identity_email_in_use", + "role_forbidden", + "organization_playground_unavailable", + "operator_unknown", + "operator_forbidden", + "operator_scope", + "email_not_confirmed", + "invitation_not_found", + "invitation_expired", + "already_a_member", + "last_owner", + "role_not_available", + "personal_account", + "invitation_pending", + "too_many_pending_invitations", + "member_not_found", + "account_has_paid_credit", + "account_has_live_reservation", + "account_has_dedicated_pool", + "already_a_partner", + "not_a_partner", + "gateway_taken", + "unknown_price_version", + "model_not_priced", + "go_live_evidence_missing", + "notice_too_short", + "stop_scope_invalid", + "invoiced_billing_off", + "invoiced_balance_outstanding", + "partner_has_own_book", + "unknown_price_book", + "operation_not_retryable" ], "title": "ApiErrorCode", "type": "string" @@ -1148,7 +1182,7 @@ }, "ModelReasoning": { "additionalProperties": false, - "description": "How this model reasons (docs/reference/gateway.md, Reasoning budget). levels: the native reasoning_effort levels, empty for a model that only switches thinking on or off; default_level: the level a request naming none runs at; thinking_off: whether reasoning_effort none turns thinking off; thinking_ceiling_tokens: the safety ceiling on thinking; budget_enforced: whether max_tokens caps the answer with thinking budgeted separately (reasoning.max_tokens a hard thinking limit).", + "description": "How this model reasons (docs/reference/models.md, Reasoning levels and thinking ceilings). levels: the native reasoning_effort levels, empty for a model that only switches thinking on or off; default_level: the level a request naming none runs at; thinking_off: whether reasoning_effort none turns thinking off; thinking_ceiling_tokens: the safety ceiling on thinking; budget_enforced: whether max_tokens caps the answer with thinking budgeted separately (reasoning.max_tokens a hard thinking limit).", "properties": { "budget_enforced": { "type": "boolean" @@ -1413,7 +1447,7 @@ "type": "integer" }, "datacenters": { - "description": "Where the model is served from: one entry per country its serving nodes are in. OpenRouter's legacy provider field (openrouter.ai/docs 'Provider Integration (Legacy Format)'), added by the gateway to an entry whose model is described to OpenRouter; OpenAI clients ignore it.", + "description": "Where the model is served from: one entry per country. OpenRouter's legacy provider field (openrouter.ai/docs 'Provider Integration (Legacy Format)'), added by the gateway to an entry whose model is described to OpenRouter; OpenAI clients ignore it.", "items": { "$ref": "#/components/schemas/OpenRouterDatacenter" }, @@ -2313,7 +2347,7 @@ }, "/v1/me": { "get": { - "description": "`rcli whoami`. Authenticated by the API key the CLI was issued, because\nthat is the only credential it has.\n\nThe contract declares that key as `BearerAuth`. The same Authorization\nheader also carries the console's signed PropelAuth session token, and\nwhere PropelAuth is not configured, never in production, the `X-User-Id`\nand `X-User-Email` development identity headers are accepted instead.\nNeither is a separate security scheme: this contract declares no console\nsession scheme on any operation, and the development headers are not a\ncredential.", + "description": "`rcli whoami`. Authenticated by the API key the CLI was issued, because\nthat is the only credential it has.\n\nThe contract declares that key as `BearerAuth`. The same Authorization\nheader also carries the console session token, and where no identity\nprovider is configured, never in production, the `X-User-Id`\nand `X-User-Email` development identity headers are accepted instead.\nNeither is a separate security scheme: this contract declares no console\nsession scheme on any operation, and the development headers are not a\ncredential.", "operationId": "getCurrentIdentity", "parameters": [ { @@ -2357,6 +2391,7 @@ }, "/v1/models": { "get": { + "description": "With a key, the models that key may call. With no credentials at all, every model this deployment serves publicly, in the same format. An invalid or revoked key is refused 401, never answered with the public list.", "operationId": "listModels", "responses": { "200": { @@ -2367,7 +2402,15 @@ } } }, - "description": "Available model aliases" + "description": "Available model aliases", + "headers": { + "x-request-id": { + "description": "Request correlation id", + "schema": { + "$ref": "#/components/schemas/RequestId" + } + } + } }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -2382,9 +2425,10 @@ "security": [ { "BearerAuth": [] - } + }, + {} ], - "summary": "List models available to the current key", + "summary": "List models", "tags": [ "inference" ] @@ -2392,7 +2436,7 @@ }, "/v1/models/catalog": { "get": { - "description": "Which models we serve and what they cost.\n\nThe console used to name a single model from a config constant, which was\ntrue when the endpoint served one and became wrong the moment LiteLLM sat in\nfront of three. Prices come from the same catalog the gate charges against,\nso a published price cannot drift from a billed one.", + "description": "Which models we serve and what they cost.\n\nThe console used to name a single model from a config constant, which was\ntrue when the endpoint served one and became wrong the moment LiteLLM sat in\nfront of three. Prices come from the same catalog the gate charges against,\nso a published price cannot drift from a billed one.\n\nAnonymous, and the same answer for everyone, except that a signed-in account also sees the\nprivate models granted to it (`private_grants.models_by_account`), named and priced, so the\nconsole does not show its usage of one as \"Retired\". The credential is read from the request\nand not declared, so the published contract is unchanged; a caller who cannot be shown to be\nsigned in gets the anonymous answer, never an error, and nothing is made for them\n(`security.optional_account`). When no grant exists at all nothing is queried for the caller.", "operationId": "getModelCatalog", "responses": { "200": { @@ -2417,7 +2461,7 @@ }, "/v1/requests/{request_id}/cancel": { "post": { - "description": "End an in-flight request the caller's key started (#440).\n\n`request_id` is the `x-request-id` the customer received on the response\nbeing cancelled. The GPU nodes are asked on this service's behalf with the\ncredential they already hold for this ledger (see services/cancel.py), and\neach ends the request only if that key started it. Anything else -- an id\nnobody is serving, a request that already finished, someone else's -- is\nthe same 404, so this route cannot be used to learn about other customers'\nrequests. A successful cancel is 202: the node has told the engine to stop;\nthe stream's end and its settlement follow on their own path.", + "description": "End an in-flight request the caller's key started (#440).\n\n`request_id` is the `x-request-id` the customer received on the response\nbeing cancelled. In `push` mode (the default) the meters are asked on this\nservice's behalf with an ID token for their own address (see services/cancel.py). In `pull`\nmode (`RA_CANCEL_MODE`, ADR 0449) this service calls nothing: it writes a cancel intent that\neach registered cell fetches, and waits up to 3 seconds for the cell's answer. Either way each\nnode ends the request only if that key started it, and anything else -- an id nobody is\nserving, a request that already finished, someone else's, or (pull) a cancel no\ncell confirmed in time -- is the same 404, so this route cannot be used to learn\nabout other customers' requests. A successful cancel is 202: the node has told\nthe engine to stop; the stream's end and its settlement follow on their own path.", "operationId": "cancelRequest", "parameters": [ { @@ -2441,6 +2485,23 @@ "422": { "$ref": "#/components/responses/ValidationFailed" }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + }, + "description": "The key has made 30 cancel calls in the last minute, or too many of its calls (4) or this instance's (64) are waiting (only when cancel_mode is pull); ask again after the Retry-After", + "headers": { + "Retry-After": { + "schema": { + "type": "string" + } + } + } + }, "default": { "$ref": "#/components/responses/ApiError" } @@ -2459,8 +2520,8 @@ }, "x-runanywhere-source": { "artifact": "contracts/control-plane-v1.openapi.json", - "branch": "fix/codex-harness-live", - "commit": "3fb1fd00aa4045ee9f2f6bfc5b7fefd6062dbc52", + "branch": "track/api-parity", + "commit": "a82593928943186f0191ae957c78184d1669c9a9", "repository": "RunanywhereAI/InferenceInfra" } } diff --git a/src/account/console_contract.rs b/src/account/console_contract.rs index d5eccd7f..693040d6 100644 --- a/src/account/console_contract.rs +++ b/src/account/console_contract.rs @@ -10,7 +10,7 @@ use serde_json::Value; /// SHA-256 of contracts/wally-cli-v1.openapi.json this file was built from. pub const CONTRACT_SHA256: &str = - "298e6c27507aaffc032ea8c2f0640d8e1acf8873baadbb8f481ee4d5f8853d47"; + "4484691b7291848efc9519a9b9af7b7946135eb071ebda744c63fc8dbc78e941"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ApiErrorCode { @@ -51,6 +51,40 @@ pub enum ApiErrorCode { KReconciliationUnverified, KRetentionManagedByRunanywhere, KCardRequired, + KInvalidAccountId, + KAccountNotFound, + KIdentityEmailInUse, + KRoleForbidden, + KOrganizationPlaygroundUnavailable, + KOperatorUnknown, + KOperatorForbidden, + KOperatorScope, + KEmailNotConfirmed, + KInvitationNotFound, + KInvitationExpired, + KAlreadyAMember, + KLastOwner, + KRoleNotAvailable, + KPersonalAccount, + KInvitationPending, + KTooManyPendingInvitations, + KMemberNotFound, + KAccountHasPaidCredit, + KAccountHasLiveReservation, + KAccountHasDedicatedPool, + KAlreadyAPartner, + KNotAPartner, + KGatewayTaken, + KUnknownPriceVersion, + KModelNotPriced, + KGoLiveEvidenceMissing, + KNoticeTooShort, + KStopScopeInvalid, + KInvoicedBillingOff, + KInvoicedBalanceOutstanding, + KPartnerHasOwnBook, + KUnknownPriceBook, + KOperationNotRetryable, } impl ApiErrorCode { @@ -92,6 +126,42 @@ impl ApiErrorCode { ApiErrorCode::KReconciliationUnverified => "reconciliation_unverified", ApiErrorCode::KRetentionManagedByRunanywhere => "retention_managed_by_runanywhere", ApiErrorCode::KCardRequired => "card_required", + ApiErrorCode::KInvalidAccountId => "invalid_account_id", + ApiErrorCode::KAccountNotFound => "account_not_found", + ApiErrorCode::KIdentityEmailInUse => "identity_email_in_use", + ApiErrorCode::KRoleForbidden => "role_forbidden", + ApiErrorCode::KOrganizationPlaygroundUnavailable => { + "organization_playground_unavailable" + } + ApiErrorCode::KOperatorUnknown => "operator_unknown", + ApiErrorCode::KOperatorForbidden => "operator_forbidden", + ApiErrorCode::KOperatorScope => "operator_scope", + ApiErrorCode::KEmailNotConfirmed => "email_not_confirmed", + ApiErrorCode::KInvitationNotFound => "invitation_not_found", + ApiErrorCode::KInvitationExpired => "invitation_expired", + ApiErrorCode::KAlreadyAMember => "already_a_member", + ApiErrorCode::KLastOwner => "last_owner", + ApiErrorCode::KRoleNotAvailable => "role_not_available", + ApiErrorCode::KPersonalAccount => "personal_account", + ApiErrorCode::KInvitationPending => "invitation_pending", + ApiErrorCode::KTooManyPendingInvitations => "too_many_pending_invitations", + ApiErrorCode::KMemberNotFound => "member_not_found", + ApiErrorCode::KAccountHasPaidCredit => "account_has_paid_credit", + ApiErrorCode::KAccountHasLiveReservation => "account_has_live_reservation", + ApiErrorCode::KAccountHasDedicatedPool => "account_has_dedicated_pool", + ApiErrorCode::KAlreadyAPartner => "already_a_partner", + ApiErrorCode::KNotAPartner => "not_a_partner", + ApiErrorCode::KGatewayTaken => "gateway_taken", + ApiErrorCode::KUnknownPriceVersion => "unknown_price_version", + ApiErrorCode::KModelNotPriced => "model_not_priced", + ApiErrorCode::KGoLiveEvidenceMissing => "go_live_evidence_missing", + ApiErrorCode::KNoticeTooShort => "notice_too_short", + ApiErrorCode::KStopScopeInvalid => "stop_scope_invalid", + ApiErrorCode::KInvoicedBillingOff => "invoiced_billing_off", + ApiErrorCode::KInvoicedBalanceOutstanding => "invoiced_balance_outstanding", + ApiErrorCode::KPartnerHasOwnBook => "partner_has_own_book", + ApiErrorCode::KUnknownPriceBook => "unknown_price_book", + ApiErrorCode::KOperationNotRetryable => "operation_not_retryable", } } @@ -133,6 +203,42 @@ impl ApiErrorCode { "reconciliation_unverified" => Ok(ApiErrorCode::KReconciliationUnverified), "retention_managed_by_runanywhere" => Ok(ApiErrorCode::KRetentionManagedByRunanywhere), "card_required" => Ok(ApiErrorCode::KCardRequired), + "invalid_account_id" => Ok(ApiErrorCode::KInvalidAccountId), + "account_not_found" => Ok(ApiErrorCode::KAccountNotFound), + "identity_email_in_use" => Ok(ApiErrorCode::KIdentityEmailInUse), + "role_forbidden" => Ok(ApiErrorCode::KRoleForbidden), + "organization_playground_unavailable" => { + Ok(ApiErrorCode::KOrganizationPlaygroundUnavailable) + } + "operator_unknown" => Ok(ApiErrorCode::KOperatorUnknown), + "operator_forbidden" => Ok(ApiErrorCode::KOperatorForbidden), + "operator_scope" => Ok(ApiErrorCode::KOperatorScope), + "email_not_confirmed" => Ok(ApiErrorCode::KEmailNotConfirmed), + "invitation_not_found" => Ok(ApiErrorCode::KInvitationNotFound), + "invitation_expired" => Ok(ApiErrorCode::KInvitationExpired), + "already_a_member" => Ok(ApiErrorCode::KAlreadyAMember), + "last_owner" => Ok(ApiErrorCode::KLastOwner), + "role_not_available" => Ok(ApiErrorCode::KRoleNotAvailable), + "personal_account" => Ok(ApiErrorCode::KPersonalAccount), + "invitation_pending" => Ok(ApiErrorCode::KInvitationPending), + "too_many_pending_invitations" => Ok(ApiErrorCode::KTooManyPendingInvitations), + "member_not_found" => Ok(ApiErrorCode::KMemberNotFound), + "account_has_paid_credit" => Ok(ApiErrorCode::KAccountHasPaidCredit), + "account_has_live_reservation" => Ok(ApiErrorCode::KAccountHasLiveReservation), + "account_has_dedicated_pool" => Ok(ApiErrorCode::KAccountHasDedicatedPool), + "already_a_partner" => Ok(ApiErrorCode::KAlreadyAPartner), + "not_a_partner" => Ok(ApiErrorCode::KNotAPartner), + "gateway_taken" => Ok(ApiErrorCode::KGatewayTaken), + "unknown_price_version" => Ok(ApiErrorCode::KUnknownPriceVersion), + "model_not_priced" => Ok(ApiErrorCode::KModelNotPriced), + "go_live_evidence_missing" => Ok(ApiErrorCode::KGoLiveEvidenceMissing), + "notice_too_short" => Ok(ApiErrorCode::KNoticeTooShort), + "stop_scope_invalid" => Ok(ApiErrorCode::KStopScopeInvalid), + "invoiced_billing_off" => Ok(ApiErrorCode::KInvoicedBillingOff), + "invoiced_balance_outstanding" => Ok(ApiErrorCode::KInvoicedBalanceOutstanding), + "partner_has_own_book" => Ok(ApiErrorCode::KPartnerHasOwnBook), + "unknown_price_book" => Ok(ApiErrorCode::KUnknownPriceBook), + "operation_not_retryable" => Ok(ApiErrorCode::KOperationNotRetryable), other => Err(format!("unknown ApiErrorCode: {other}")), } }