From 783e0393c102753b810102596c807a71b94118b9 Mon Sep 17 00:00:00 2001 From: "zoo-github-actions-auth[bot]" Date: Mon, 10 Aug 2026 14:59:03 +0000 Subject: [PATCH 1/2] YOYO NEW API SPEC! --- spec.json | 327 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 326 insertions(+), 1 deletion(-) diff --git a/spec.json b/spec.json index 224cf3290..b053de4fd 100644 --- a/spec.json +++ b/spec.json @@ -43144,6 +43144,15 @@ "description": "The content of the user's message.", "type": "string" }, + "correlation_id": { + "nullable": true, + "description": "Stable identifier used to correlate this user request across services.", + "allOf": [ + { + "$ref": "#/components/schemas/Uuid" + } + ] + }, "current_files": { "description": "The current files in the project, if any. This can be used to provide context for the AI. This should be sent in binary format, if the files are not text files, like an imported binary file.", "type": "object", @@ -43156,6 +43165,15 @@ } } }, + "engine_api_call_id": { + "nullable": true, + "description": "API call ID for the active Engine modeling session, when available.", + "allOf": [ + { + "$ref": "#/components/schemas/Uuid" + } + ] + }, "forced_tools": { "description": "The user can force specific tools to be used for this message.", "type": "array", @@ -43730,6 +43748,19 @@ ], "additionalProperties": false }, + { + "description": "Backend-only token usage and cost for one completed Zookeeper turn.\n\nSent just before `EndOfStream`. API records it in `meta.usage` on the turn's `EndOfStream` message row, alongside the `meta.billing` revenue figures, and never forwards it to clients or replays it as a chat message. It exists so spend can be compared against what the turn was billed; it is not customer-facing.", + "type": "object", + "properties": { + "zookeeper_turn_usage": { + "$ref": "#/components/schemas/ZookeeperTurnUsage" + } + }, + "required": [ + "zookeeper_turn_usage" + ], + "additionalProperties": false + }, { "description": "Backend-only completed tool result used for portable Zookeeper recovery.\n\nAPI persists this message and includes it only in replay sent to the text-to-CAD backend. It is never forwarded to browser clients.", "type": "object", @@ -43768,7 +43799,7 @@ "additionalProperties": false }, { - "description": "Replay containing raw bytes for previously-saved messages for a conversation. Includes server messages and client `User` messages.\n\nInvariants: - Client replay includes server messages: `Info`, `Error`, `Reasoning(..)`, `ToolOutput { .. }`, `Files { .. }`, `ProjectUpdated { .. }`, and `EndOfStream { .. }`. - Client replay also includes client `User` messages. - Backend replay includes client `User` messages plus selected reasoning, edit metadata, recovery output, and final responses. - The following are NEVER included: `SessionData`, `ConversationId`, `Delta`, `BackendShutdown`, or `ZookeeperAutoRouterMetadata`. - `ZookeeperRecoveryToolOutput` is included only in replay sent to the text-to-CAD backend and is filtered from client replay. - Ordering is stable: messages are ordered by prompt creation time within the conversation, then by the per-prompt `seq` value (monotonically increasing as seen in the original stream).\n\nWire format: - Each element is canonical serialized bytes (typically JSON) for either a `MlCopilotServerMessage` or a `MlCopilotClientMessage::User`. - When delivered as an initial replay over the websocket (upon `?replay=true&conversation_id=`), the server sends a single WebSocket Binary frame containing a MsgPack-encoded document of this enum: `Replay { messages }`.", + "description": "Replay containing raw bytes for previously-saved messages for a conversation. Includes server messages and client `User` messages.\n\nInvariants: - Client replay includes server messages: `Info`, `Error`, `Reasoning(..)`, `ToolOutput { .. }`, `Files { .. }`, `ProjectUpdated { .. }`, and `EndOfStream { .. }`. - Client replay also includes client `User` messages. - Backend replay includes client `User` messages plus selected reasoning, edit metadata, recovery output, and final responses. - The following are NEVER included: `SessionData`, `ConversationId`, `Delta`, `BackendShutdown`, `ZookeeperAutoRouterMetadata`, or `ZookeeperTurnUsage`. - `ZookeeperRecoveryToolOutput` is included only in replay sent to the text-to-CAD backend and is filtered from client replay. - Ordering is stable: messages are ordered by prompt creation time within the conversation, then by the per-prompt `seq` value (monotonically increasing as seen in the original stream).\n\nWire format: - Each element is canonical serialized bytes (typically JSON) for either a `MlCopilotServerMessage` or a `MlCopilotClientMessage::User`. - When delivered as an initial replay over the websocket (upon `?replay=true&conversation_id=`), the server sends a single WebSocket Binary frame containing a MsgPack-encoded document of this enum: `Replay { messages }`.", "type": "object", "properties": { "replay": { @@ -63602,6 +63633,300 @@ } ] }, + "ZookeeperTurnUsage": { + "description": "Token usage and cost for one completed Zookeeper turn.\n\nA turn is many model calls: the streamed agent loop, helper agents, conversation compaction, Auto-mode routing, and any retried attempts. This is the sum of all of them, with breakdowns by model and by stage.", + "type": "object", + "properties": { + "api_call_id": { + "nullable": true, + "description": "API call the turn belongs to, as the backend knows it.", + "type": "string" + }, + "cache_write_input_tokens": { + "description": "Prompt tokens written to the provider's prompt cache.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "cached_input_tokens": { + "description": "Prompt tokens served from the provider's prompt cache.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "conversation_id": { + "nullable": true, + "description": "Conversation the turn belongs to, as the backend knows it.", + "type": "string" + }, + "cost_micro_usd": { + "description": "Cost in millionths of a US dollar, rounded half-up.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "cost_usd": { + "description": "Exact cost as a decimal string, e.g. \"0.02184000\".", + "default": "", + "type": "string" + }, + "fully_priced": { + "description": "Whether every request was priced.", + "default": false, + "type": "boolean" + }, + "input_tokens": { + "description": "Prompt tokens, including the cached and cache-write subsets below.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "models": { + "description": "Usage split by model.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ZookeeperTurnUsageModel" + } + }, + "output_tokens": { + "description": "Completion tokens, including the reasoning subset below.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "priced_requests": { + "description": "Requests whose model had a known price.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "reasoning_tokens": { + "description": "Completion tokens spent on reasoning.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "requests": { + "description": "Number of model requests.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "schema_version": { + "description": "Payload shape identifier, e.g. \"zookeeper_turn_usage.v1\".", + "default": "", + "type": "string" + }, + "stages": { + "description": "Usage split by the part of the turn that spent it.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ZookeeperTurnUsageStage" + } + }, + "total_tokens": { + "description": "Input plus output tokens.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "uncached_input_tokens": { + "description": "Prompt tokens billed at the full input rate.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "unpriced_requests": { + "description": "Requests whose model had no known price; their tokens are counted but contribute no cost, so totals are a floor rather than an estimate.", + "default": 0, + "type": "integer", + "format": "int64" + } + } + }, + "ZookeeperTurnUsageModel": { + "description": "Per-model usage within a Zookeeper turn.", + "type": "object", + "properties": { + "cache_write_input_tokens": { + "description": "Prompt tokens written to the provider's prompt cache.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "cached_input_tokens": { + "description": "Prompt tokens served from the provider's prompt cache.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "cost_micro_usd": { + "description": "Cost in millionths of a US dollar, rounded half-up.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "cost_usd": { + "description": "Exact cost as a decimal string, e.g. \"0.02184000\".", + "default": "", + "type": "string" + }, + "fully_priced": { + "description": "Whether every request was priced.", + "default": false, + "type": "boolean" + }, + "input_tokens": { + "description": "Prompt tokens, including the cached and cache-write subsets below.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "model": { + "description": "Model name as the provider reported it.", + "type": "string" + }, + "output_tokens": { + "description": "Completion tokens, including the reasoning subset below.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "priced_requests": { + "description": "Requests whose model had a known price.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "pricing_source": { + "description": "Where the price came from, e.g. \"litellm\", \"litellm_alias\", \"override\", or \"unknown\".", + "default": "", + "type": "string" + }, + "reasoning_tokens": { + "description": "Completion tokens spent on reasoning.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "requests": { + "description": "Number of model requests.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "total_tokens": { + "description": "Input plus output tokens.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "uncached_input_tokens": { + "description": "Prompt tokens billed at the full input rate.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "unpriced_requests": { + "description": "Requests whose model had no known price; their tokens are counted but contribute no cost, so totals are a floor rather than an estimate.", + "default": 0, + "type": "integer", + "format": "int64" + } + }, + "required": [ + "model" + ] + }, + "ZookeeperTurnUsageStage": { + "description": "Per-stage usage within a Zookeeper turn.", + "type": "object", + "properties": { + "cache_write_input_tokens": { + "description": "Prompt tokens written to the provider's prompt cache.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "cached_input_tokens": { + "description": "Prompt tokens served from the provider's prompt cache.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "cost_micro_usd": { + "description": "Cost in millionths of a US dollar, rounded half-up.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "cost_usd": { + "description": "Exact cost as a decimal string, e.g. \"0.02184000\".", + "default": "", + "type": "string" + }, + "fully_priced": { + "description": "Whether every request was priced.", + "default": false, + "type": "boolean" + }, + "input_tokens": { + "description": "Prompt tokens, including the cached and cache-write subsets below.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "output_tokens": { + "description": "Completion tokens, including the reasoning subset below.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "priced_requests": { + "description": "Requests whose model had a known price.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "reasoning_tokens": { + "description": "Completion tokens spent on reasoning.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "requests": { + "description": "Number of model requests.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "stage": { + "description": "Which part of the turn spent the tokens, e.g. \"agent_stream\", \"auto_router\", \"compaction\", \"sub_agent\", or \"prompt_validation\".", + "type": "string" + }, + "total_tokens": { + "description": "Input plus output tokens.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "uncached_input_tokens": { + "description": "Prompt tokens billed at the full input rate.", + "default": 0, + "type": "integer", + "format": "int64" + }, + "unpriced_requests": { + "description": "Requests whose model had no known price; their tokens are counted but contribute no cost, so totals are a floor rather than an estimate.", + "default": 0, + "type": "integer", + "format": "int64" + } + }, + "required": [ + "stage" + ] + }, "ZoomToFit": { "description": "The response from the `ZoomToFit` command.", "type": "object", From 30e705ad88a089c4e60dc6cf807dca73c8dc4ae3 Mon Sep 17 00:00:00 2001 From: "zoo-github-actions-auth[bot]" Date: Mon, 10 Aug 2026 14:59:50 +0000 Subject: [PATCH 2/2] I have generated the latest API! --- kittycad.py.patch.json | 10 +- kittycad/models/__init__.py | 3 + kittycad/models/ml_copilot_client_message.py | 4 + kittycad/models/ml_copilot_server_message.py | 12 +- kittycad/models/zookeeper_turn_usage.py | 47 +++++++ kittycad/models/zookeeper_turn_usage_model.py | 37 +++++ kittycad/models/zookeeper_turn_usage_stage.py | 35 +++++ kittycad/tests/test_examples.py | 130 +++++------------- 8 files changed, 174 insertions(+), 104 deletions(-) create mode 100644 kittycad/models/zookeeper_turn_usage.py create mode 100644 kittycad/models/zookeeper_turn_usage_model.py create mode 100644 kittycad/models/zookeeper_turn_usage_stage.py diff --git a/kittycad.py.patch.json b/kittycad.py.patch.json index 7282d9e2f..54970b07d 100644 --- a/kittycad.py.patch.json +++ b/kittycad.py.patch.json @@ -155,7 +155,7 @@ "op": "add", "path": "/paths/~1file~1conversion/post/x-python", "value": { - "example": "from kittycad.models.conversion_params import ConversionParams\nfrom kittycad.models.output_format3d import OptionStl\nfrom kittycad.models.system import System\nfrom kittycad.models.axis_direction_pair import AxisDirectionPair\nfrom kittycad.models.axis import Axis\nfrom kittycad.models.direction import Direction\nfrom kittycad.models.axis_direction_pair import AxisDirectionPair\nfrom kittycad.models.axis import Axis\nfrom kittycad.models.direction import Direction\nfrom kittycad.models.selection import OptionSceneByIndex\nfrom kittycad.models.selection import Selection\nfrom kittycad.models.stl_storage import StlStorage\nfrom kittycad.models.unit_length import UnitLength\nfrom kittycad.models.output_format3d import OutputFormat3d\nfrom kittycad.models.input_format3d import OptionInventor\nfrom kittycad.models.system import System\nfrom kittycad.models.axis_direction_pair import AxisDirectionPair\nfrom kittycad.models.axis import Axis\nfrom kittycad.models.direction import Direction\nfrom kittycad.models.axis_direction_pair import AxisDirectionPair\nfrom kittycad.models.axis import Axis\nfrom kittycad.models.direction import Direction\nfrom kittycad.models.input_format3d import InputFormat3d\nfrom pathlib import Path\nfrom typing import Dict\nfrom kittycad._io_types import SyncUpload\nfrom kittycad.models import FileConversion\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_create_file_conversion_options():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n result: FileConversion = client.file.create_file_conversion_options(body=ConversionParams(\n output_format=OutputFormat3d(OptionStl(\n coords=System(\n forward=AxisDirectionPair(\n axis=Axis.Y,\n direction=Direction.POSITIVE,\n ),\n up=AxisDirectionPair(\n axis=Axis.Y,\n direction=Direction.POSITIVE,\n ),\n ),\n selection=Selection(OptionSceneByIndex(\n index=10,\n )),\n storage=StlStorage.ASCII,\n units=UnitLength.CM,\n )),\n src_format=InputFormat3d(OptionInventor(\n coords=System(\n forward=AxisDirectionPair(\n axis=Axis.Y,\n direction=Direction.POSITIVE,\n ),\n up=AxisDirectionPair(\n axis=Axis.Y,\n direction=Direction.POSITIVE,\n ),\n ),\n split_closed_faces=False,\n )),\n ),\n file_attachments={\n \"main.kcl\": Path(\"path/to/main.kcl\"),\n \"helper.kcl\": Path(\"path/to/helper.kcl\"),\n })\n\n\n body: FileConversion = result\n print(body)\n\n", + "example": "from kittycad.models.conversion_params import ConversionParams\nfrom kittycad.models.output_format3d import OptionGltf\nfrom kittycad.models.gltf_presentation import GltfPresentation\nfrom kittycad.models.gltf_storage import GltfStorage\nfrom kittycad.models.output_format3d import OutputFormat3d\nfrom kittycad.models.input_format3d import OptionFbx\nfrom kittycad.models.input_format3d import InputFormat3d\nfrom pathlib import Path\nfrom typing import Dict\nfrom kittycad._io_types import SyncUpload\nfrom kittycad.models import FileConversion\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_create_file_conversion_options():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n result: FileConversion = client.file.create_file_conversion_options(body=ConversionParams(\n output_format=OutputFormat3d(OptionGltf(\n presentation=GltfPresentation.COMPACT,\n storage=GltfStorage.BINARY,\n )),\n src_format=InputFormat3d(OptionFbx()),\n ),\n file_attachments={\n \"main.kcl\": Path(\"path/to/main.kcl\"),\n \"helper.kcl\": Path(\"path/to/helper.kcl\"),\n })\n\n\n body: FileConversion = result\n print(body)\n\n", "libDocsLink": "https://python.api.docs.zoo.dev/_autosummary/kittycad.KittyCAD.html#kittycad.KittyCAD.file" } }, @@ -771,7 +771,7 @@ "op": "add", "path": "/paths/~1org~1saml~1idp/post/x-python", "value": { - "example": "from kittycad.models.saml_identity_provider_create import SamlIdentityProviderCreate\nfrom kittycad.models.idp_metadata_source import OptionUrl\nfrom kittycad.models.idp_metadata_source import IdpMetadataSource\nfrom kittycad.models import SamlIdentityProvider\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_create_org_saml_idp():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n result: SamlIdentityProvider = client.orgs.create_org_saml_idp(body=SamlIdentityProviderCreate(\n idp_entity_id=\"\",\n idp_metadata_source=IdpMetadataSource(OptionUrl(\n url=\"\",\n )),\n technical_contact_email=\"\",\n ))\n\n\n body: SamlIdentityProvider = result\n print(body)\n\n", + "example": "from kittycad.models.saml_identity_provider_create import SamlIdentityProviderCreate\nfrom kittycad.models.idp_metadata_source import OptionBase64EncodedXml\nfrom kittycad.models.base64data import Base64Data\nfrom kittycad.models.idp_metadata_source import IdpMetadataSource\nfrom kittycad.models import SamlIdentityProvider\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_create_org_saml_idp():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n result: SamlIdentityProvider = client.orgs.create_org_saml_idp(body=SamlIdentityProviderCreate(\n idp_entity_id=\"\",\n idp_metadata_source=IdpMetadataSource(OptionBase64EncodedXml(\n data=Base64Data(b\"\"),\n )),\n technical_contact_email=\"\",\n ))\n\n\n body: SamlIdentityProvider = result\n print(body)\n\n", "libDocsLink": "https://python.api.docs.zoo.dev/_autosummary/kittycad.KittyCAD.html#kittycad.KittyCAD.orgs" } }, @@ -1691,7 +1691,7 @@ "op": "add", "path": "/paths/~1ws~1ml~1copilot/get/x-python", "value": { - "example": "from kittycad.models import MlCopilotClientMessage\nfrom kittycad.models.ml_copilot_client_message import OptionListModes\nfrom kittycad.models import MlCopilotServerMessage\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_ml_copilot_ws():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n # Connect to the websocket.\n with client.ml.ml_copilot_ws(replay=None,\n conversation_id=None,\n pr=None) as websocket:\n\n # Send a message.\n websocket.send(MlCopilotClientMessage(OptionListModes()))\n\n # Get a message.\n message = websocket.recv()\n print(message)\n\n ", + "example": "from kittycad.models import MlCopilotClientMessage\nfrom kittycad.models.ml_copilot_client_message import OptionProjectContext\nfrom kittycad.models import MlCopilotServerMessage\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_ml_copilot_ws():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n # Connect to the websocket.\n with client.ml.ml_copilot_ws(replay=None,\n conversation_id=None,\n pr=None) as websocket:\n\n # Send a message.\n websocket.send(MlCopilotClientMessage(OptionProjectContext(\ncurrent_files={\"\": b\"\"},\n)))\n\n # Get a message.\n message = websocket.recv()\n print(message)\n\n ", "libDocsLink": "https://python.api.docs.zoo.dev/_autosummary/kittycad.KittyCAD.html#kittycad.KittyCAD.ml" } }, @@ -1699,7 +1699,7 @@ "op": "add", "path": "/paths/~1ws~1ml~1reasoning~1{id}/get/x-python", "value": { - "example": "from kittycad.models import MlCopilotClientMessage\nfrom kittycad.models.ml_copilot_client_message import OptionProjectContext\nfrom kittycad.models import MlCopilotServerMessage\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_ml_reasoning_ws():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n # Connect to the websocket.\n with client.ml.ml_reasoning_ws(id=\"\") as websocket:\n\n # Send a message.\n websocket.send(MlCopilotClientMessage(OptionProjectContext(\ncurrent_files={\"\": b\"\"},\n)))\n\n # Get a message.\n message = websocket.recv()\n print(message)\n\n ", + "example": "from kittycad.models import MlCopilotClientMessage\nfrom kittycad.models.ml_copilot_client_message import OptionListModes\nfrom kittycad.models import MlCopilotServerMessage\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_ml_reasoning_ws():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n # Connect to the websocket.\n with client.ml.ml_reasoning_ws(id=\"\") as websocket:\n\n # Send a message.\n websocket.send(MlCopilotClientMessage(OptionListModes()))\n\n # Get a message.\n message = websocket.recv()\n print(message)\n\n ", "libDocsLink": "https://python.api.docs.zoo.dev/_autosummary/kittycad.KittyCAD.html#kittycad.KittyCAD.ml" } }, @@ -1707,7 +1707,7 @@ "op": "add", "path": "/paths/~1ws~1modeling~1commands/get/x-python", "value": { - "example": "from kittycad.models.post_effect_type import PostEffectType\nfrom kittycad.models import WebSocketRequest\nfrom kittycad.models.web_socket_request import OptionTrickleIce\nfrom kittycad.models.rtc_ice_candidate_init import RtcIceCandidateInit\nfrom kittycad.models import WebSocketResponse\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_modeling_commands_ws():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n # Connect to the websocket.\n with client.modeling.modeling_commands_ws(video_res_width=10,\n video_res_height=10,\n fps=10,\n unlocked_framerate=False,\n post_effect=PostEffectType.PHOSPHOR,\n webrtc=False,\n show_grid=False,\n order_independent_transparency=False,\n pool=None,\n replay=None,\n api_call_id=None,\n pr=None) as websocket:\n\n # Send a message.\n websocket.send(WebSocketRequest(OptionTrickleIce(\ncandidate=RtcIceCandidateInit(\ncandidate=\"\",\n),\n)))\n\n # Get a message.\n message = websocket.recv()\n print(message)\n\n ", + "example": "from kittycad.models.post_effect_type import PostEffectType\nfrom kittycad.models import WebSocketRequest\nfrom kittycad.models.web_socket_request import OptionDebug\nfrom kittycad.models import WebSocketResponse\nfrom typing import Union, Any, Optional, List, Tuple\nfrom kittycad.types import Response\ndef example_modeling_commands_ws():\n client = KittyCAD() # Uses KITTYCAD_API_TOKEN environment variable\n\n # Connect to the websocket.\n with client.modeling.modeling_commands_ws(video_res_width=10,\n video_res_height=10,\n fps=10,\n unlocked_framerate=False,\n post_effect=PostEffectType.PHOSPHOR,\n webrtc=False,\n show_grid=False,\n order_independent_transparency=False,\n pool=None,\n replay=None,\n api_call_id=None,\n pr=None) as websocket:\n\n # Send a message.\n websocket.send(WebSocketRequest(OptionDebug()))\n\n # Get a message.\n message = websocket.recv()\n print(message)\n\n ", "libDocsLink": "https://python.api.docs.zoo.dev/_autosummary/kittycad.KittyCAD.html#kittycad.KittyCAD.modeling" } } diff --git a/kittycad/models/__init__.py b/kittycad/models/__init__.py index d4ba43ebe..696497d94 100644 --- a/kittycad/models/__init__.py +++ b/kittycad/models/__init__.py @@ -572,4 +572,7 @@ from .zookeeper_auto_router_metadata import ZookeeperAutoRouterMetadata from .zookeeper_edit_patch import ZookeeperEditPatch from .zookeeper_edit_patch_file import ZookeeperEditPatchFile +from .zookeeper_turn_usage import ZookeeperTurnUsage +from .zookeeper_turn_usage_model import ZookeeperTurnUsageModel +from .zookeeper_turn_usage_stage import ZookeeperTurnUsageStage from .zoom_to_fit import ZoomToFit diff --git a/kittycad/models/ml_copilot_client_message.py b/kittycad/models/ml_copilot_client_message.py index 633e5644e..6fe97f5fe 100644 --- a/kittycad/models/ml_copilot_client_message.py +++ b/kittycad/models/ml_copilot_client_message.py @@ -51,8 +51,12 @@ class OptionUser(KittyCadBaseModel): content: str + correlation_id: Optional[Uuid] = None + current_files: Optional[Dict[str, bytes]] = None + engine_api_call_id: Optional[Uuid] = None + forced_tools: Optional[List[MlCopilotTool]] = None mode: Optional[MlCopilotMode] = None diff --git a/kittycad/models/ml_copilot_server_message.py b/kittycad/models/ml_copilot_server_message.py index 1afc6f9e8..29efba4b7 100644 --- a/kittycad/models/ml_copilot_server_message.py +++ b/kittycad/models/ml_copilot_server_message.py @@ -11,6 +11,7 @@ from ..models.zookeeper_auto_router_metadata import ( ZookeeperAutoRouterMetadata as ZookeeperAutoRouterMetadataModel, ) +from ..models.zookeeper_turn_usage import ZookeeperTurnUsage as ZookeeperTurnUsageModel from .base import KittyCadBaseModel @@ -310,6 +311,14 @@ class ZookeeperAutoRouterMetadata(KittyCadBaseModel): zookeeper_auto_router_metadata: ZookeeperAutoRouterMetadataModel +class ZookeeperTurnUsage(KittyCadBaseModel): + """Backend-only token usage and cost for one completed Zookeeper turn. + + Sent just before `EndOfStream`. API records it in `meta.usage` on the turn's `EndOfStream` message row, alongside the `meta.billing` revenue figures, and never forwards it to clients or replays it as a chat message. It exists so spend can be compared against what the turn was billed; it is not customer-facing.""" + + zookeeper_turn_usage: ZookeeperTurnUsageModel + + class ZookeeperRecoveryToolOutput(KittyCadBaseModel): """Backend-only completed tool result used for portable Zookeeper recovery. @@ -345,7 +354,7 @@ def _wrap(self, handler, info): class Replay(KittyCadBaseModel): """Replay containing raw bytes for previously-saved messages for a conversation. Includes server messages and client `User` messages. - Invariants: - Client replay includes server messages: `Info`, `Error`, `Reasoning(..)`, `ToolOutput { .. }`, `Files { .. }`, `ProjectUpdated { .. }`, and `EndOfStream { .. }`. - Client replay also includes client `User` messages. - Backend replay includes client `User` messages plus selected reasoning, edit metadata, recovery output, and final responses. - The following are NEVER included: `SessionData`, `ConversationId`, `Delta`, `BackendShutdown`, or `ZookeeperAutoRouterMetadata`. - `ZookeeperRecoveryToolOutput` is included only in replay sent to the text-to-CAD backend and is filtered from client replay. - Ordering is stable: messages are ordered by prompt creation time within the conversation, then by the per-prompt `seq` value (monotonically increasing as seen in the original stream). + Invariants: - Client replay includes server messages: `Info`, `Error`, `Reasoning(..)`, `ToolOutput { .. }`, `Files { .. }`, `ProjectUpdated { .. }`, and `EndOfStream { .. }`. - Client replay also includes client `User` messages. - Backend replay includes client `User` messages plus selected reasoning, edit metadata, recovery output, and final responses. - The following are NEVER included: `SessionData`, `ConversationId`, `Delta`, `BackendShutdown`, `ZookeeperAutoRouterMetadata`, or `ZookeeperTurnUsage`. - `ZookeeperRecoveryToolOutput` is included only in replay sent to the text-to-CAD backend and is filtered from client replay. - Ordering is stable: messages are ordered by prompt creation time within the conversation, then by the per-prompt `seq` value (monotonically increasing as seen in the original stream). Wire format: - Each element is canonical serialized bytes (typically JSON) for either a `MlCopilotServerMessage` or a `MlCopilotClientMessage::User`. - When delivered as an initial replay over the websocket (upon `?replay=true&conversation_id=`), the server sends a single WebSocket Binary frame containing a MsgPack-encoded document of this enum: `Replay { messages }`.""" @@ -442,6 +451,7 @@ def _wrap(self, handler, info): RequestAttachments, AttachmentsLoaded, ZookeeperAutoRouterMetadata, + ZookeeperTurnUsage, ZookeeperRecoveryToolOutput, Replay, EndOfStream, diff --git a/kittycad/models/zookeeper_turn_usage.py b/kittycad/models/zookeeper_turn_usage.py new file mode 100644 index 000000000..042e8519a --- /dev/null +++ b/kittycad/models/zookeeper_turn_usage.py @@ -0,0 +1,47 @@ +from typing import List, Optional + +from ..models.zookeeper_turn_usage_model import ZookeeperTurnUsageModel +from ..models.zookeeper_turn_usage_stage import ZookeeperTurnUsageStage +from .base import KittyCadBaseModel + + +class ZookeeperTurnUsage(KittyCadBaseModel): + """Token usage and cost for one completed Zookeeper turn. + + A turn is many model calls: the streamed agent loop, helper agents, conversation compaction, Auto-mode routing, and any retried attempts. This is the sum of all of them, with breakdowns by model and by stage.""" + + api_call_id: Optional[str] = None + + cache_write_input_tokens: Optional[int] = 0 + + cached_input_tokens: Optional[int] = 0 + + conversation_id: Optional[str] = None + + cost_micro_usd: Optional[int] = 0 + + cost_usd: Optional[str] = "" + + fully_priced: Optional[bool] = False + + input_tokens: Optional[int] = 0 + + models: Optional[List[ZookeeperTurnUsageModel]] = None + + output_tokens: Optional[int] = 0 + + priced_requests: Optional[int] = 0 + + reasoning_tokens: Optional[int] = 0 + + requests: Optional[int] = 0 + + schema_version: Optional[str] = "" + + stages: Optional[List[ZookeeperTurnUsageStage]] = None + + total_tokens: Optional[int] = 0 + + uncached_input_tokens: Optional[int] = 0 + + unpriced_requests: Optional[int] = 0 diff --git a/kittycad/models/zookeeper_turn_usage_model.py b/kittycad/models/zookeeper_turn_usage_model.py new file mode 100644 index 000000000..81c816e61 --- /dev/null +++ b/kittycad/models/zookeeper_turn_usage_model.py @@ -0,0 +1,37 @@ +from typing import Optional + +from .base import KittyCadBaseModel + + +class ZookeeperTurnUsageModel(KittyCadBaseModel): + """Per-model usage within a Zookeeper turn.""" + + cache_write_input_tokens: Optional[int] = 0 + + cached_input_tokens: Optional[int] = 0 + + cost_micro_usd: Optional[int] = 0 + + cost_usd: Optional[str] = "" + + fully_priced: Optional[bool] = False + + input_tokens: Optional[int] = 0 + + model: str + + output_tokens: Optional[int] = 0 + + priced_requests: Optional[int] = 0 + + pricing_source: Optional[str] = "" + + reasoning_tokens: Optional[int] = 0 + + requests: Optional[int] = 0 + + total_tokens: Optional[int] = 0 + + uncached_input_tokens: Optional[int] = 0 + + unpriced_requests: Optional[int] = 0 diff --git a/kittycad/models/zookeeper_turn_usage_stage.py b/kittycad/models/zookeeper_turn_usage_stage.py new file mode 100644 index 000000000..2debc647f --- /dev/null +++ b/kittycad/models/zookeeper_turn_usage_stage.py @@ -0,0 +1,35 @@ +from typing import Optional + +from .base import KittyCadBaseModel + + +class ZookeeperTurnUsageStage(KittyCadBaseModel): + """Per-stage usage within a Zookeeper turn.""" + + cache_write_input_tokens: Optional[int] = 0 + + cached_input_tokens: Optional[int] = 0 + + cost_micro_usd: Optional[int] = 0 + + cost_usd: Optional[str] = "" + + fully_priced: Optional[bool] = False + + input_tokens: Optional[int] = 0 + + output_tokens: Optional[int] = 0 + + priced_requests: Optional[int] = 0 + + reasoning_tokens: Optional[int] = 0 + + requests: Optional[int] = 0 + + stage: str + + total_tokens: Optional[int] = 0 + + uncached_input_tokens: Optional[int] = 0 + + unpriced_requests: Optional[int] = 0 diff --git a/kittycad/tests/test_examples.py b/kittycad/tests/test_examples.py index e597aeef4..39bbae3d4 100644 --- a/kittycad/tests/test_examples.py +++ b/kittycad/tests/test_examples.py @@ -95,8 +95,7 @@ AggregateUsageCollectionThresholdSet, ) from kittycad.models.api_token_uuid import ApiTokenUuid -from kittycad.models.axis import Axis -from kittycad.models.axis_direction_pair import AxisDirectionPair +from kittycad.models.base64data import Base64Data from kittycad.models.billing_cadence import BillingCadence from kittycad.models.billing_commitment_scope import BillingCommitmentScope from kittycad.models.billing_contract_item_input import BillingContractItemInput @@ -126,15 +125,20 @@ from kittycad.models.create_shortlink_request import CreateShortlinkRequest from kittycad.models.created_at_sort_mode import CreatedAtSortMode from kittycad.models.currency import Currency -from kittycad.models.direction import Direction from kittycad.models.email_authentication_form import EmailAuthenticationForm from kittycad.models.email_marketing_confirm_token_body import ( EmailMarketingConfirmTokenBody, ) from kittycad.models.file_export_format import FileExportFormat from kittycad.models.file_import_format import FileImportFormat -from kittycad.models.idp_metadata_source import IdpMetadataSource, OptionUrl -from kittycad.models.input_format3d import InputFormat3d, OptionInventor +from kittycad.models.gltf_presentation import GltfPresentation +from kittycad.models.gltf_storage import GltfStorage +from kittycad.models.idp_metadata_source import ( + IdpMetadataSource, + OptionBase64EncodedXml, + OptionUrl, +) +from kittycad.models.input_format3d import InputFormat3d, OptionFbx from kittycad.models.kcl_code_completion_params import KclCodeCompletionParams from kittycad.models.kcl_code_completion_request import KclCodeCompletionRequest from kittycad.models.kcl_project_share_link_access_mode import ( @@ -149,7 +153,7 @@ from kittycad.models.o_auth2_app_grant_type import OAuth2AppGrantType from kittycad.models.org_dataset_source import OrgDatasetSource from kittycad.models.org_details import OrgDetails -from kittycad.models.output_format3d import OptionStl, OutputFormat3d +from kittycad.models.output_format3d import OptionGltf, OutputFormat3d from kittycad.models.plan_interval import PlanInterval from kittycad.models.post_effect_type import PostEffectType from kittycad.models.price_upsert_request import PriceUpsertRequest @@ -161,21 +165,17 @@ from kittycad.models.public_mailing_list_membership_request import ( PublicMailingListMembershipRequest, ) -from kittycad.models.rtc_ice_candidate_init import RtcIceCandidateInit from kittycad.models.sales_inquiry_type import SalesInquiryType from kittycad.models.saml_identity_provider_create import SamlIdentityProviderCreate -from kittycad.models.selection import OptionSceneByIndex, Selection from kittycad.models.service_account_uuid import ServiceAccountUuid from kittycad.models.session_uuid import SessionUuid from kittycad.models.source_position import SourcePosition from kittycad.models.source_range import SourceRange from kittycad.models.source_range_prompt import SourceRangePrompt -from kittycad.models.stl_storage import StlStorage from kittycad.models.storage_provider import StorageProvider from kittycad.models.store_coupon_params import StoreCouponParams from kittycad.models.subscription_plan_billing_model import SubscriptionPlanBillingModel from kittycad.models.support_inquiry_type import SupportInquiryType -from kittycad.models.system import System from kittycad.models.text_to_cad_create_body import TextToCadCreateBody from kittycad.models.text_to_cad_iteration_body import TextToCadIterationBody from kittycad.models.text_to_cad_multi_file_iteration_body import ( @@ -205,7 +205,7 @@ from kittycad.models.user_identifier import UserIdentifier from kittycad.models.user_org_role import UserOrgRole from kittycad.models.uuid import Uuid -from kittycad.models.web_socket_request import OptionTrickleIce +from kittycad.models.web_socket_request import OptionDebug from kittycad.models.website_sales_form import WebsiteSalesForm from kittycad.models.website_support_form import WebsiteSupportForm from kittycad.models.zoo_product_subscriptions_org_request import ( @@ -594,41 +594,12 @@ def test_create_file_conversion_options(): result: FileConversion = client.file.create_file_conversion_options( body=ConversionParams( output_format=OutputFormat3d( - OptionStl( - coords=System( - forward=AxisDirectionPair( - axis=Axis.Y, - direction=Direction.POSITIVE, - ), - up=AxisDirectionPair( - axis=Axis.Y, - direction=Direction.POSITIVE, - ), - ), - selection=Selection( - OptionSceneByIndex( - index=10, - ) - ), - storage=StlStorage.ASCII, - units=UnitLength.CM, - ) - ), - src_format=InputFormat3d( - OptionInventor( - coords=System( - forward=AxisDirectionPair( - axis=Axis.Y, - direction=Direction.POSITIVE, - ), - up=AxisDirectionPair( - axis=Axis.Y, - direction=Direction.POSITIVE, - ), - ), - split_closed_faces=False, + OptionGltf( + presentation=GltfPresentation.COMPACT, + storage=GltfStorage.BINARY, ) ), + src_format=InputFormat3d(OptionFbx()), ), file_attachments={ "main.kcl": Path("path/to/main.kcl"), @@ -649,41 +620,12 @@ async def test_create_file_conversion_options_async(): result: FileConversion = await client.file.create_file_conversion_options( body=ConversionParams( output_format=OutputFormat3d( - OptionStl( - coords=System( - forward=AxisDirectionPair( - axis=Axis.Y, - direction=Direction.POSITIVE, - ), - up=AxisDirectionPair( - axis=Axis.Y, - direction=Direction.POSITIVE, - ), - ), - selection=Selection( - OptionSceneByIndex( - index=10, - ) - ), - storage=StlStorage.ASCII, - units=UnitLength.CM, - ) - ), - src_format=InputFormat3d( - OptionInventor( - coords=System( - forward=AxisDirectionPair( - axis=Axis.Y, - direction=Direction.POSITIVE, - ), - up=AxisDirectionPair( - axis=Axis.Y, - direction=Direction.POSITIVE, - ), - ), - split_closed_faces=False, + OptionGltf( + presentation=GltfPresentation.COMPACT, + storage=GltfStorage.BINARY, ) ), + src_format=InputFormat3d(OptionFbx()), ), file_attachments={ "main.kcl": Path("path/to/main.kcl"), @@ -2452,8 +2394,8 @@ def test_create_org_saml_idp(): body=SamlIdentityProviderCreate( idp_entity_id="", idp_metadata_source=IdpMetadataSource( - OptionUrl( - url="", + OptionBase64EncodedXml( + data=Base64Data(b""), ) ), technical_contact_email="", @@ -2474,8 +2416,8 @@ async def test_create_org_saml_idp_async(): body=SamlIdentityProviderCreate( idp_entity_id="", idp_metadata_source=IdpMetadataSource( - OptionUrl( - url="", + OptionBase64EncodedXml( + data=Base64Data(b""), ) ), technical_contact_email="", @@ -5339,7 +5281,13 @@ def test_ml_copilot_ws(): replay=None, conversation_id=None, pr=None ) as websocket: # Send a message. - websocket.send(MlCopilotClientMessage(OptionListModes())) + websocket.send( + MlCopilotClientMessage( + OptionProjectContext( + current_files={"": b""}, + ) + ) + ) # Get a message. message = websocket.recv() @@ -5372,13 +5320,7 @@ def test_ml_reasoning_ws(): # Connect to the websocket. with client.ml.ml_reasoning_ws(id="") as websocket: # Send a message. - websocket.send( - MlCopilotClientMessage( - OptionProjectContext( - current_files={"": b""}, - ) - ) - ) + websocket.send(MlCopilotClientMessage(OptionListModes())) # Get a message. message = websocket.recv() @@ -5422,15 +5364,7 @@ def test_modeling_commands_ws(): pr=None, ) as websocket: # Send a message. - websocket.send( - WebSocketRequest( - OptionTrickleIce( - candidate=RtcIceCandidateInit( - candidate="", - ), - ) - ) - ) + websocket.send(WebSocketRequest(OptionDebug())) # Get a message. message = websocket.recv()