From 55613c256044464e283c2327e99a4d216bb23dda Mon Sep 17 00:00:00 2001 From: speakeasybot Date: Wed, 12 Aug 2026 19:08:13 +0000 Subject: [PATCH 1/2] ## Python SDK Changes: * `open_router.datasets.get_session_cost()`: **Added** --- .speakeasy/gen.lock | 107 ++++- .speakeasy/gen.yaml | 2 +- .speakeasy/out.openapi.yaml | 214 ++++++++++ .speakeasy/workflow.lock | 10 +- RELEASES.md | 12 +- docs/components/sessioncostitem.mdx | 13 + docs/components/sessioncostmeta.mdx | 12 + docs/components/sessioncostmetaversion.mdx | 17 + docs/components/sessioncostresponse.mdx | 10 + docs/components/turnrange.mdx | 24 ++ docs/operations/getsessioncostglobals.mdx | 11 + docs/operations/getsessioncostrequest.mdx | 16 + docs/operations/getsessioncostresponse.mdx | 9 + docs/operations/turnrange.mdx | 24 ++ docs/sdks/datasets/README.mdx | 62 +++ pyproject.toml | 2 +- src/openrouter/_version.py | 4 +- src/openrouter/components/__init__.py | 23 ++ src/openrouter/components/sessioncostitem.py | 48 +++ src/openrouter/components/sessioncostmeta.py | 50 +++ .../components/sessioncostresponse.py | 19 + src/openrouter/datasets.py | 375 ++++++++++++++++++ src/openrouter/operations/__init__.py | 23 ++ src/openrouter/operations/getsessioncost.py | 227 +++++++++++ uv.lock | 2 +- 25 files changed, 1286 insertions(+), 30 deletions(-) create mode 100644 docs/components/sessioncostitem.mdx create mode 100644 docs/components/sessioncostmeta.mdx create mode 100644 docs/components/sessioncostmetaversion.mdx create mode 100644 docs/components/sessioncostresponse.mdx create mode 100644 docs/components/turnrange.mdx create mode 100644 docs/operations/getsessioncostglobals.mdx create mode 100644 docs/operations/getsessioncostrequest.mdx create mode 100644 docs/operations/getsessioncostresponse.mdx create mode 100644 docs/operations/turnrange.mdx create mode 100644 src/openrouter/components/sessioncostitem.py create mode 100644 src/openrouter/components/sessioncostmeta.py create mode 100644 src/openrouter/components/sessioncostresponse.py create mode 100644 src/openrouter/operations/getsessioncost.py diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index 260182f2..548e3e77 100644 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -1,19 +1,19 @@ lockVersion: 2.0.0 id: c48cf606-fb42-4a45-9c23-8f0555307828 management: - docChecksum: 0e4089c3ee0487407460fb15d2de8cb2 + docChecksum: 343efae9999aeb995bfc47cdf1956a93 docVersion: 1.0.0 speakeasyVersion: 1.787.0 generationVersion: 2.914.0 - releaseVersion: 1.1.49 - configChecksum: b6fa1bdede75022d704e0eed5a39d54f + releaseVersion: 1.1.50 + configChecksum: 11fe028f4c27fde842ab1cba02a48b81 repoURL: https://github.com/OpenRouterTeam/python-sdk.git installationURL: https://github.com/OpenRouterTeam/python-sdk.git published: true persistentEdits: - generation_id: 5f71a04f-f018-4a06-bd14-0802c3e597a1 - pristine_commit_hash: f744573be1b70bf1fe63c71c9fa4cd90e604b54d - pristine_tree_hash: 1dae52a10738aa268bc2d9c1b5381825d92c338d + generation_id: 37461a55-30c5-4d3f-bf59-25987190f80a + pristine_commit_hash: 5ef983e7b820f60c431c85c30b3598010dd52c9b + pristine_tree_hash: eb86cc69bb30fc4486afb4790131bce74cb99d1a features: python: acceptHeaders: 3.0.0 @@ -4716,6 +4716,22 @@ trackedFiles: id: 72f01d77e774 last_write_checksum: sha1:5ca87c689aa95fc476ccf60129736a3c5c8021fe pristine_git_object: e4da707cb407e833a9a541405e7c546b78d990aa + docs/components/sessioncostitem.mdx: + id: 50daab7870c5 + last_write_checksum: sha1:9a9cb133798d50646502dc87eb052ecfa6fab533 + pristine_git_object: 6bab396f00b46ad95f0d842ae2f06af51880282e + docs/components/sessioncostmeta.mdx: + id: a11f69bf41cb + last_write_checksum: sha1:133d9513f8416cd01af0c424a3d47d3ba46a6c18 + pristine_git_object: f27d0ca7b88c692ce5a0f14288e9a85157b864ca + docs/components/sessioncostmetaversion.mdx: + id: d304756a4d45 + last_write_checksum: sha1:0d69d099b6f7547fbce431c3025f90ef1237a732 + pristine_git_object: de98461ac2c5d7ca8d6ccb5bdc897c815a227176 + docs/components/sessioncostresponse.mdx: + id: 6d7d7e41fc60 + last_write_checksum: sha1:1492a00174e054356ecb3c8a6bb542f065ffc1d1 + pristine_git_object: 4b94d8dffe97ab1b43231a40ae4947bc5bd86dc1 docs/components/shellcallitem.mdx: id: 172eabd6add3 last_write_checksum: sha1:a6a305a8589a1ec230110e823639d7ce13b568cd @@ -5180,6 +5196,10 @@ trackedFiles: id: d338e8ec18bd last_write_checksum: sha1:c8bc50c016758d48320643ce2965fedba9b1e3f1 pristine_git_object: 6b46b4c151e73cf9efeb4bcbfb0343258a0ed9d5 + docs/components/turnrange.mdx: + id: d4f863eecbe8 + last_write_checksum: sha1:3cc0bd9a3336447a3ba36639b4af2dff530d33ac + pristine_git_object: f713faa59d39701cafff13c07ed8995782187bd7 docs/components/type.mdx: id: d15dd4f8a580 last_write_checksum: sha1:9ab10f9a767e94d7da3f931b62a4f5bc36da9e4c @@ -6408,6 +6428,18 @@ trackedFiles: id: d5f5f6747522 last_write_checksum: sha1:f86eff956c41225004a47350e31bfb0842f6b968 pristine_git_object: 65e59b601a99b58683e4af1f7302a21c9ac3ce04 + docs/operations/getsessioncostglobals.mdx: + id: 9d0a1df1aa96 + last_write_checksum: sha1:bd37b2d3bdb691f88c20db056035a03d6c332ca1 + pristine_git_object: 8ec32402d0625ee81c3af2b00858f244f0838ff7 + docs/operations/getsessioncostrequest.mdx: + id: dda19cf3b11a + last_write_checksum: sha1:f407a690ca88c50b666054b4c47d3f0cdfc84000 + pristine_git_object: 41b333db846fbda52b0e48c9e0ea5263cef2e815 + docs/operations/getsessioncostresponse.mdx: + id: 2a527f1c393c + last_write_checksum: sha1:a6b457552b59686e40bbf234728e2996a082e148 + pristine_git_object: d79786dc84fcbc6f9409266750e42f134791fd4d docs/operations/gettaskclassificationsglobals.mdx: id: 14d0010b40f8 last_write_checksum: sha1:6c967e8855540035a6f70fa895ceea0c9caf1cdd @@ -6952,6 +6984,10 @@ trackedFiles: id: 6694d3af2ace last_write_checksum: sha1:bc7319f009e4133e65cf476c1763b11fb6622ff9 pristine_git_object: c783ad06042f710374b4702b3b63030a32e52180 + docs/operations/turnrange.mdx: + id: 2bea5698a6d4 + last_write_checksum: sha1:b19bf13e5fa27e57f649d21548e750cbdc280a61 + pristine_git_object: e540a723a617f8381c0ca905c12456bf1f4804e0 docs/operations/typeimageurl.mdx: id: 66578c3ea6a5 last_write_checksum: sha1:e3275995c5351075e0c4082a161891cf710b8fdc @@ -7114,8 +7150,8 @@ trackedFiles: pristine_git_object: 414ae2830c58add7ad904cc9ffc5b46ed65058df docs/sdks/datasets/README.mdx: id: 15e5ab0183de - last_write_checksum: sha1:ea0bec2975d9b64dea2d22a2851c35880532cd3f - pristine_git_object: fe3a93c6cff82a397a3c26811e6cbd7a681bb489 + last_write_checksum: sha1:fdb99109a1beb492222426112999718444fbdb8e + pristine_git_object: e71813682dd01efd21e06bc8804086d6fde5e1ea docs/sdks/embeddings/README.mdx: id: cd71581df291 last_write_checksum: sha1:810a851cac701dfa79d8130a38396d0a1b8b5196 @@ -7198,8 +7234,8 @@ trackedFiles: pristine_git_object: 3e38f1a929f7d6b1d6de74604aa87e3d8f010544 pyproject.toml: id: 5d07e7d72637 - last_write_checksum: sha1:f2dfd308858eaad1971f10de6edcdd91d672d0f3 - pristine_git_object: 8feb96e7c3197e4d02defc99c811ebe12dc01ce3 + last_write_checksum: sha1:c0a35fb23a30a0a95fa826dd11c7027df8a6a80b + pristine_git_object: 0c7c8dc146d6b1242ca3d6253f27412952191f23 scripts/prepare_readme.py: id: e0c5957a6035 last_write_checksum: sha1:77f44b60b98bc126557ec27391f91dfba764bb54 @@ -7226,8 +7262,8 @@ trackedFiles: pristine_git_object: 86713cfea633e09d33b3d4e65281071fe20e6137 src/openrouter/_version.py: id: d8d15ad6c586 - last_write_checksum: sha1:7712e65eb5296def159efebc89fa565db4e4d77a - pristine_git_object: 9c36c309cc613438f636181b3e07ff9f527d77b8 + last_write_checksum: sha1:fe3c197afb053d094e073dccb812579b596db21f + pristine_git_object: a1f69c761e373b9237783a8ff1d2a83a06d1a9a4 src/openrouter/analytics.py: id: cb406b5aaabb last_write_checksum: sha1:3ea0f1c73fb9c101b7bc5719da4dd7bf62ff469a @@ -7270,8 +7306,8 @@ trackedFiles: pristine_git_object: ad3d247954547814054c01989a2dff3d12b3e4e1 src/openrouter/components/__init__.py: id: 81754e97b3f4 - last_write_checksum: sha1:53e2861e42f8094758a01a20eba2b39a4cd1cb91 - pristine_git_object: e2409b0beded65a52ecd777135d3f040030ef699 + last_write_checksum: sha1:f1dfb69c3b3296a121af19e198dc431be62d9138 + pristine_git_object: aa1609dc43dfbd4a6f64d49dd70fe272fc21e4c3 src/openrouter/components/aabenchmarkentry.py: id: e2e0f0b48c82 last_write_checksum: sha1:fab4d9a24d2cea937bb749d46c5f83941e99d65c @@ -9280,6 +9316,18 @@ trackedFiles: id: 606d51d201e8 last_write_checksum: sha1:8fdac755a1b710330e9ed8b1ecb41bef1b4d2ab1 pristine_git_object: cd18828d5d77f1b0f171990ea07693494e90260c + src/openrouter/components/sessioncostitem.py: + id: ccdd8c31ecad + last_write_checksum: sha1:edbe1b560bc9fcd58029e1d80f859a3e320726f2 + pristine_git_object: 56f631568dc1d732acd9691f0f9d3eae8c73c356 + src/openrouter/components/sessioncostmeta.py: + id: c92be58316b0 + last_write_checksum: sha1:bfa8fb1098cb5e8e3819c249bf0bec1fe4be3bd9 + pristine_git_object: ce055e4b54eefdd83035dcf20617bd35fa4ff45d + src/openrouter/components/sessioncostresponse.py: + id: 7a486a18368e + last_write_checksum: sha1:e68458021c6529e032194cc740887a45e11c7549 + pristine_git_object: e105a7eb1936e652dadb95209bfa1f5258a2fb4b src/openrouter/components/shellcallitem.py: id: 0b3b4f503413 last_write_checksum: sha1:460fb9d45c3b4f7392ef5213878c541bb0e922f2 @@ -9710,8 +9758,8 @@ trackedFiles: pristine_git_object: 791cdeba11d6c5e9e9381760fd8cd8ca86233b22 src/openrouter/datasets.py: id: 2378f80d1d6a - last_write_checksum: sha1:9d7c00a8748a3d6015be7159cfd77609ae52a741 - pristine_git_object: a1e8475671445f9d2e3b5cb7f0345ab6e4dfba7d + last_write_checksum: sha1:1a447242bb4a28d8b95415d16f662d9ddefd2547 + pristine_git_object: d153d356c5643f5c4f94d18d29adfb99f06addf5 src/openrouter/embeddings.py: id: 0374ace39067 last_write_checksum: sha1:220cac105c58c9b42f2f9f9e6bf98f6a7beb4f96 @@ -9846,8 +9894,8 @@ trackedFiles: pristine_git_object: 5c553b76ab24d9eaef51c3bf6d85aa74391e335e src/openrouter/operations/__init__.py: id: 9afcea1e7161 - last_write_checksum: sha1:1a29771015fee7983b677eb476aeb281fad9c2dc - pristine_git_object: 50ebc6f58e44ee099607fa6140232132a7990767 + last_write_checksum: sha1:040182e46f2248a17818facd01e0596af6f33a95 + pristine_git_object: 76ee91671dc2984195055b18c7bf1e49969e14bc src/openrouter/operations/bulkaddworkspacemembers.py: id: e0ed56117619 last_write_checksum: sha1:5c44eb0d40fdece3ac084615f6c6082be4cf1d5a @@ -10052,6 +10100,10 @@ trackedFiles: id: e1222ef7697e last_write_checksum: sha1:e1d1933f745296958538ae7c4bdc8ead53944492 pristine_git_object: d930ef5a4202b0ec9a88329927f7ea6373a7971f + src/openrouter/operations/getsessioncost.py: + id: d01a691a6603 + last_write_checksum: sha1:aa6af6b0570292ca32e27400e4278ce0b21b3bce + pristine_git_object: 7a5e3e1cf09fcd3ac3a50b8f5790a1d6f35e5381 src/openrouter/operations/gettaskclassifications.py: id: ba6156d68acf last_write_checksum: sha1:c569572d21aa766ac542d5c535208e8b1caae503 @@ -12081,7 +12133,24 @@ examples: application/json: {"error": {"code": 404, "message": "Resource not found"}} "500": application/json: {"error": {"code": 500, "message": "Internal Server Error"}} + getSessionCost: + speakeasy-default-get-session-cost: + parameters: + query: + limit: 100 + offset: 0 + responses: + "200": + application/json: {"data": [{"app_name": "Hermes Agent", "app_slug": "hermes-agent", "median_session_cost_usd": 1.74, "model_permaslug": "anthropic/claude-4.8-opus", "turn_range": "10-49-turns"}], "meta": {"as_of": "2026-05-12T02:00:00.000Z", "version": "v1", "window_days": 30, "window_end_date": "2026-05-11"}} + "400": + application/json: {"error": {"code": 400, "message": "Invalid request parameters"}} + "401": + application/json: {"error": {"code": 401, "message": "Missing Authentication header"}} + "429": + application/json: {"error": {"code": 429, "message": "Rate limit exceeded"}} + "500": + application/json: {"error": {"code": 500, "message": "Internal Server Error"}} examplesVersion: 1.0.2 releaseNotes: | ## Python SDK Changes: - * `open_router.workspaces.delete()`: `request.confirm_default_settings_deletion` **Added** + * `open_router.datasets.get_session_cost()`: **Added** diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index 5e98c0a0..4356023f 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -36,7 +36,7 @@ generation: documentation: mintlify preApplyUnionDiscriminators: true python: - version: 1.1.49 + version: 1.1.50 additionalDependencies: dev: {} main: {} diff --git a/.speakeasy/out.openapi.yaml b/.speakeasy/out.openapi.yaml index ed82a15a..5966c2b9 100644 --- a/.speakeasy/out.openapi.yaml +++ b/.speakeasy/out.openapi.yaml @@ -22675,6 +22675,83 @@ components: - 'code' - 'message' type: 'object' + SessionCostItem: + properties: + app_name: + description: 'Published harness display label.' + example: 'Hermes Agent' + type: 'string' + app_slug: + description: 'Stable public slug of the harness.' + example: 'hermes-agent' + type: 'string' + median_session_cost_usd: + description: 'Median USD spend per sampled session.' + example: 1.74 + format: 'double' + type: 'number' + model_permaslug: + description: 'Exact model permaslug.' + example: 'anthropic/claude-4.8-opus' + type: 'string' + turn_range: + description: 'Inclusive session turn-count range.' + enum: + - '1-turn' + - '2-9-turns' + - '10-49-turns' + - '50-plus-turns' + example: '10-49-turns' + type: 'string' + x-speakeasy-unknown-values: allow + required: + - 'app_slug' + - 'app_name' + - 'turn_range' + - 'model_permaslug' + - 'median_session_cost_usd' + type: 'object' + SessionCostMeta: + properties: + as_of: + description: 'ISO-8601 timestamp when the response was generated.' + example: '2026-05-12T02:00:00.000Z' + type: 'string' + version: + description: 'Dataset version.' + enum: + - 'v1' + type: 'string' + window_days: + description: 'Number of days in the weekly session sample window, or null when no snapshot is published.' + example: 30 + type: + - 'integer' + - 'null' + window_end_date: + description: 'UTC date of the final day in the session sample window, or null when no snapshot is published.' + example: '2026-05-11' + type: + - 'string' + - 'null' + required: + - 'as_of' + - 'version' + - 'window_days' + - 'window_end_date' + type: 'object' + SessionCostResponse: + properties: + data: + items: + $ref: '#/components/schemas/SessionCostItem' + type: 'array' + meta: + $ref: '#/components/schemas/SessionCostMeta' + required: + - 'data' + - 'meta' + type: 'object' ShellCallItem: description: 'A shell command execution call (newer variant)' example: @@ -29101,6 +29178,143 @@ paths: - $ref: "#/components/parameters/AppIdentifier" - $ref: "#/components/parameters/AppDisplayName" - $ref: "#/components/parameters/AppCategories" + /datasets/session-cost: + get: + description: "Returns weekly refreshed, aggregated cost-per-session cells for the published harnesses.\nSessions are never pooled across apps. Medians are of per-session USD spend, and\nprivacy-preserving aggregation never exposes clerk_user_id values or per-session rows.\n\nFilter by `app_slug`, `model`, or `turn_range`. Filtering by `model` alone works across apps\nfor harness-vs-harness comparison at a fixed model. Results refresh weekly and include the source snapshot\nwindow in `meta`." + operationId: 'getSessionCost' + parameters: + - description: 'Filter to one published harness slug.' + in: 'query' + name: 'app_slug' + required: false + schema: + description: 'Filter to one published harness slug.' + example: 'hermes-agent' + type: 'string' + - description: 'Exact model permaslug filter. Works across all harness apps.' + in: 'query' + name: 'model' + required: false + schema: + description: 'Exact model permaslug filter. Works across all harness apps.' + example: 'anthropic/claude-4.8-opus' + type: 'string' + - description: 'Filter by the inclusive number of turns in a session.' + in: 'query' + name: 'turn_range' + required: false + schema: + description: 'Filter by the inclusive number of turns in a session.' + enum: + - '1-turn' + - '2-9-turns' + - '10-49-turns' + - '50-plus-turns' + example: '10-49-turns' + type: 'string' + x-speakeasy-unknown-values: allow + - description: 'Maximum number of cells to return (1-500). Defaults to 100.' + in: 'query' + name: 'limit' + required: false + schema: + default: 100 + description: 'Maximum number of cells to return (1-500). Defaults to 100.' + example: 100 + maximum: 500 + minimum: 1 + type: 'integer' + - description: 'Number of sorted cells to skip (0-5000). Defaults to 0.' + in: 'query' + name: 'offset' + required: false + schema: + default: 0 + description: 'Number of sorted cells to skip (0-5000). Defaults to 0.' + example: 0 + maximum: 5000 + minimum: 0 + type: + - 'integer' + - 'null' + nullable: false + responses: + '200': + content: + application/json: + example: + data: + - app_name: 'Hermes Agent' + app_slug: 'hermes-agent' + median_session_cost_usd: 1.74 + model_permaslug: 'anthropic/claude-4.8-opus' + turn_range: '10-49-turns' + meta: + as_of: '2026-05-12T02:00:00.000Z' + version: 'v1' + window_days: 30 + window_end_date: '2026-05-11' + schema: + $ref: '#/components/schemas/SessionCostResponse' + description: 'Aggregated cost-per-session cells for the requested filters.' + '400': + content: + application/json: + example: + error: + code: 400 + message: 'Invalid request parameters' + schema: + $ref: '#/components/schemas/BadRequestResponse' + description: 'Bad Request - Invalid request parameters or malformed input' + '401': + content: + application/json: + example: + error: + code: 401 + message: 'Missing Authentication header' + schema: + $ref: '#/components/schemas/UnauthorizedResponse' + description: 'Unauthorized - Authentication required or invalid credentials' + '429': + content: + application/json: + example: + error: + code: 429 + message: 'Rate limit exceeded' + schema: + $ref: '#/components/schemas/TooManyRequestsResponse' + description: 'Too Many Requests - Rate limit exceeded' + '500': + content: + application/json: + example: + error: + code: 500 + message: 'Internal Server Error' + schema: + $ref: '#/components/schemas/InternalServerResponse' + description: 'Internal Server Error - Unexpected server error' + summary: 'Cost per session by harness and model' + tags: + - 'Datasets' + x-speakeasy-pagination: + inputs: + - in: 'parameters' + name: 'offset' + type: 'offset' + - in: 'parameters' + name: 'limit' + type: 'limit' + outputs: + results: '$.data' + type: 'offsetLimit' + parameters: + - $ref: "#/components/parameters/AppIdentifier" + - $ref: "#/components/parameters/AppDisplayName" + - $ref: "#/components/parameters/AppCategories" /embeddings: post: description: 'Submits an embedding request to the embeddings router' diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index 6114cd67..4bd7c6b5 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -2,8 +2,8 @@ speakeasyVersion: 1.787.0 sources: OpenRouter API: sourceNamespace: open-router-chat-completions-api - sourceRevisionDigest: sha256:a68f0ac37d057a36bc53f9fdb997d966a6e32a8254c2f77476265dc5e4a46678 - sourceBlobDigest: sha256:55f0c37a738f069ca8c2327e71ed1eeed0df0d3041d5d4e059d0dc80961cd8c3 + sourceRevisionDigest: sha256:5f205df13a98e8f546ad24ec24ee75abb6cff5481810439aa50a8b5470a849d4 + sourceBlobDigest: sha256:1c42ce0ffa3b4d1c0b8b5b784747a9bd79541005ce5eec51daa394c4dd91db5b tags: - latest - 1.0.0 @@ -11,10 +11,10 @@ targets: open-router: source: OpenRouter API sourceNamespace: open-router-chat-completions-api - sourceRevisionDigest: sha256:a68f0ac37d057a36bc53f9fdb997d966a6e32a8254c2f77476265dc5e4a46678 - sourceBlobDigest: sha256:55f0c37a738f069ca8c2327e71ed1eeed0df0d3041d5d4e059d0dc80961cd8c3 + sourceRevisionDigest: sha256:5f205df13a98e8f546ad24ec24ee75abb6cff5481810439aa50a8b5470a849d4 + sourceBlobDigest: sha256:1c42ce0ffa3b4d1c0b8b5b784747a9bd79541005ce5eec51daa394c4dd91db5b codeSamplesNamespace: open-router-python-code-samples - codeSamplesRevisionDigest: sha256:9a756237d69d864f6b8d2df77cd51d19d9442fe75ce096bdc8ad791e8d1868c9 + codeSamplesRevisionDigest: sha256:f10ee7e964de640bcad68d1c9886c8a4d4dddd5f9beb2ee9f8718ffdba52f913 workflow: workflowVersion: 1.0.0 speakeasyVersion: 1.787.0 diff --git a/RELEASES.md b/RELEASES.md index f6a16964..4c8b6b9c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1279,4 +1279,14 @@ Based on: ### Generated - [python v1.1.49] . ### Releases -- [PyPI v1.1.49] https://pypi.org/project/openrouter/1.1.49 - . \ No newline at end of file +- [PyPI v1.1.49] https://pypi.org/project/openrouter/1.1.49 - . + +## 2026-08-12 19:05:55 +### Changes +Based on: +- OpenAPI Doc +- Speakeasy CLI 1.787.0 (2.914.0) https://github.com/speakeasy-api/speakeasy +### Generated +- [python v1.1.50] . +### Releases +- [PyPI v1.1.50] https://pypi.org/project/openrouter/1.1.50 - . \ No newline at end of file diff --git a/docs/components/sessioncostitem.mdx b/docs/components/sessioncostitem.mdx new file mode 100644 index 00000000..6bab396f --- /dev/null +++ b/docs/components/sessioncostitem.mdx @@ -0,0 +1,13 @@ +--- +title: "SessionCostItem" +--- + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | +| `app_name` | *str* | :heavy_check_mark: | Published harness display label. | Hermes Agent | +| `app_slug` | *str* | :heavy_check_mark: | Stable public slug of the harness. | hermes-agent | +| `median_session_cost_usd` | *float* | :heavy_check_mark: | Median USD spend per sampled session. | 1.74 | +| `model_permaslug` | *str* | :heavy_check_mark: | Exact model permaslug. | anthropic/claude-4.8-opus | +| `turn_range` | [components.TurnRange](../components/turnrange.mdx) | :heavy_check_mark: | Inclusive session turn-count range. | 10-49-turns | \ No newline at end of file diff --git a/docs/components/sessioncostmeta.mdx b/docs/components/sessioncostmeta.mdx new file mode 100644 index 00000000..f27d0ca7 --- /dev/null +++ b/docs/components/sessioncostmeta.mdx @@ -0,0 +1,12 @@ +--- +title: "SessionCostMeta" +--- + +## Fields + +| Field | Type | Required | Description | Example | +| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `as_of` | *str* | :heavy_check_mark: | ISO-8601 timestamp when the response was generated. | 2026-05-12T02:00:00.000Z | +| `version` | [components.SessionCostMetaVersion](../components/sessioncostmetaversion.mdx) | :heavy_check_mark: | Dataset version. | | +| `window_days` | *Nullable[int]* | :heavy_check_mark: | Number of days in the weekly session sample window, or null when no snapshot is published. | 30 | +| `window_end_date` | *Nullable[str]* | :heavy_check_mark: | UTC date of the final day in the session sample window, or null when no snapshot is published. | 2026-05-11 | \ No newline at end of file diff --git a/docs/components/sessioncostmetaversion.mdx b/docs/components/sessioncostmetaversion.mdx new file mode 100644 index 00000000..de98461a --- /dev/null +++ b/docs/components/sessioncostmetaversion.mdx @@ -0,0 +1,17 @@ +--- +title: "SessionCostMetaVersion" +--- + +Dataset version. + +## Example Usage + +```python +from openrouter.components import SessionCostMetaVersion +value: SessionCostMetaVersion = "v1" +``` + + +## Values + +- `"v1"` diff --git a/docs/components/sessioncostresponse.mdx b/docs/components/sessioncostresponse.mdx new file mode 100644 index 00000000..4b94d8df --- /dev/null +++ b/docs/components/sessioncostresponse.mdx @@ -0,0 +1,10 @@ +--- +title: "SessionCostResponse" +--- + +## Fields + +| Field | Type | Required | Description | +| -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `data` | List[[components.SessionCostItem](../components/sessioncostitem.mdx)] | :heavy_check_mark: | N/A | +| `meta` | [components.SessionCostMeta](../components/sessioncostmeta.mdx) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/docs/components/turnrange.mdx b/docs/components/turnrange.mdx new file mode 100644 index 00000000..f713faa5 --- /dev/null +++ b/docs/components/turnrange.mdx @@ -0,0 +1,24 @@ +--- +title: "TurnRange" +--- + +Inclusive session turn-count range. + +## Example Usage + +```python +from openrouter.components import TurnRange + +# Open enum: unrecognized values are captured as UnrecognizedStr +value: TurnRange = "1-turn" +``` + + +## Values + +This is an open enum. Unrecognized values will not fail type checks. + +- `"1-turn"` +- `"2-9-turns"` +- `"10-49-turns"` +- `"50-plus-turns"` diff --git a/docs/operations/getsessioncostglobals.mdx b/docs/operations/getsessioncostglobals.mdx new file mode 100644 index 00000000..8ec32402 --- /dev/null +++ b/docs/operations/getsessioncostglobals.mdx @@ -0,0 +1,11 @@ +--- +title: "GetSessionCostGlobals" +--- + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `http_referer` | *Optional[str]* | :heavy_minus_sign: | The app identifier should be your app's URL and is used as the primary identifier for rankings.
This is used to track API usage per application.
| +| `x_open_router_title` | *Optional[str]* | :heavy_minus_sign: | The app display name allows you to customize how your app appears in OpenRouter's dashboard.
| +| `x_open_router_categories` | *Optional[str]* | :heavy_minus_sign: | Comma-separated list of app categories (e.g. "cli-agent,cloud-agent"). Used for marketplace rankings.
| \ No newline at end of file diff --git a/docs/operations/getsessioncostrequest.mdx b/docs/operations/getsessioncostrequest.mdx new file mode 100644 index 00000000..41b333db --- /dev/null +++ b/docs/operations/getsessioncostrequest.mdx @@ -0,0 +1,16 @@ +--- +title: "GetSessionCostRequest" +--- + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `http_referer` | *Optional[str]* | :heavy_minus_sign: | The app identifier should be your app's URL and is used as the primary identifier for rankings.
This is used to track API usage per application.
| | +| `x_open_router_title` | *Optional[str]* | :heavy_minus_sign: | The app display name allows you to customize how your app appears in OpenRouter's dashboard.
| | +| `x_open_router_categories` | *Optional[str]* | :heavy_minus_sign: | Comma-separated list of app categories (e.g. "cli-agent,cloud-agent"). Used for marketplace rankings.
| | +| `app_slug` | *Optional[str]* | :heavy_minus_sign: | Filter to one published harness slug. | hermes-agent | +| `model` | *Optional[str]* | :heavy_minus_sign: | Exact model permaslug filter. Works across all harness apps. | anthropic/claude-4.8-opus | +| `turn_range` | [Optional[operations.TurnRange]](../operations/turnrange.mdx) | :heavy_minus_sign: | Filter by the inclusive number of turns in a session. | 10-49-turns | +| `limit` | *Optional[int]* | :heavy_minus_sign: | Maximum number of cells to return (1-500). Defaults to 100. | 100 | +| `offset` | *OptionalNullable[int]* | :heavy_minus_sign: | Number of sorted cells to skip (0-5000). Defaults to 0. | 0 | \ No newline at end of file diff --git a/docs/operations/getsessioncostresponse.mdx b/docs/operations/getsessioncostresponse.mdx new file mode 100644 index 00000000..d79786dc --- /dev/null +++ b/docs/operations/getsessioncostresponse.mdx @@ -0,0 +1,9 @@ +--- +title: "GetSessionCostResponse" +--- + +## Fields + +| Field | Type | Required | Description | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `result` | [components.SessionCostResponse](../components/sessioncostresponse.mdx) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/docs/operations/turnrange.mdx b/docs/operations/turnrange.mdx new file mode 100644 index 00000000..e540a723 --- /dev/null +++ b/docs/operations/turnrange.mdx @@ -0,0 +1,24 @@ +--- +title: "TurnRange" +--- + +Filter by the inclusive number of turns in a session. + +## Example Usage + +```python +from openrouter.operations import TurnRange + +# Open enum: unrecognized values are captured as UnrecognizedStr +value: TurnRange = "1-turn" +``` + + +## Values + +This is an open enum. Unrecognized values will not fail type checks. + +- `"1-turn"` +- `"2-9-turns"` +- `"10-49-turns"` +- `"50-plus-turns"` diff --git a/docs/sdks/datasets/README.mdx b/docs/sdks/datasets/README.mdx index fe3a93c6..e7181368 100644 --- a/docs/sdks/datasets/README.mdx +++ b/docs/sdks/datasets/README.mdx @@ -11,6 +11,7 @@ Datasets endpoints * [get_app_rankings](#get_app_rankings) - Top apps by token usage * [get_rankings_daily](#get_rankings_daily) - Daily token totals for top 50 models +* [get_session_cost](#get_session_cost) - Cost per session by harness and model ## get_app_rankings @@ -163,6 +164,67 @@ with OpenRouter( ### Errors +| Error Type | Status Code | Content Type | +| ----------------------------------- | ----------------------------------- | ----------------------------------- | +| errors.BadRequestResponseError | 400 | application/json | +| errors.UnauthorizedResponseError | 401 | application/json | +| errors.TooManyRequestsResponseError | 429 | application/json | +| errors.InternalServerResponseError | 500 | application/json | +| errors.OpenRouterDefaultError | 4XX, 5XX | \*/\* | + +## get_session_cost + +Returns weekly refreshed, aggregated cost-per-session cells for the published harnesses. +Sessions are never pooled across apps. Medians are of per-session USD spend, and +privacy-preserving aggregation never exposes clerk_user_id values or per-session rows. + +Filter by `app_slug`, `model`, or `turn_range`. Filtering by `model` alone works across apps +for harness-vs-harness comparison at a fixed model. Results refresh weekly and include the source snapshot +window in `meta`. + +### Example Usage + +```python +from openrouter import OpenRouter +import os + + +with OpenRouter( + http_referer="", + x_open_router_title="", + x_open_router_categories="", + api_key=os.getenv("OPENROUTER_API_KEY", ""), +) as open_router: + + res = open_router.datasets.get_session_cost(limit=100, offset=0) + + while res is not None: + # Handle items + + res = res.next() + +``` + +### Parameters + +| Parameter | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `http_referer` | *Optional[str]* | :heavy_minus_sign: | The app identifier should be your app's URL and is used as the primary identifier for rankings.
This is used to track API usage per application.
| | +| `x_open_router_title` | *Optional[str]* | :heavy_minus_sign: | The app display name allows you to customize how your app appears in OpenRouter's dashboard.
| | +| `x_open_router_categories` | *Optional[str]* | :heavy_minus_sign: | Comma-separated list of app categories (e.g. "cli-agent,cloud-agent"). Used for marketplace rankings.
| | +| `app_slug` | *Optional[str]* | :heavy_minus_sign: | Filter to one published harness slug. | hermes-agent | +| `model` | *Optional[str]* | :heavy_minus_sign: | Exact model permaslug filter. Works across all harness apps. | anthropic/claude-4.8-opus | +| `turn_range` | [Optional[operations.TurnRange]](../../operations/turnrange.mdx) | :heavy_minus_sign: | Filter by the inclusive number of turns in a session. | 10-49-turns | +| `limit` | *Optional[int]* | :heavy_minus_sign: | Maximum number of cells to return (1-500). Defaults to 100. | 100 | +| `offset` | *OptionalNullable[int]* | :heavy_minus_sign: | Number of sorted cells to skip (0-5000). Defaults to 0. | 0 | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.mdx) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | + +### Response + +**[operations.GetSessionCostResponse](../../operations/getsessioncostresponse.mdx)** + +### Errors + | Error Type | Status Code | Content Type | | ----------------------------------- | ----------------------------------- | ----------------------------------- | | errors.BadRequestResponseError | 400 | application/json | diff --git a/pyproject.toml b/pyproject.toml index 8feb96e7..0c7c8dc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openrouter" -version = "1.1.49" +version = "1.1.50" description = "Official Python Client SDK for OpenRouter." authors = [{ name = "OpenRouter" },] readme = "README-PYPI.md" diff --git a/src/openrouter/_version.py b/src/openrouter/_version.py index 9c36c309..a1f69c76 100644 --- a/src/openrouter/_version.py +++ b/src/openrouter/_version.py @@ -3,10 +3,10 @@ import importlib.metadata __title__: str = "openrouter" -__version__: str = "1.1.49" +__version__: str = "1.1.50" __openapi_doc_version__: str = "1.0.0" __gen_version__: str = "2.914.0" -__user_agent__: str = "speakeasy-sdk/python 1.1.49 2.914.0 1.0.0 openrouter" +__user_agent__: str = "speakeasy-sdk/python 1.1.50 2.914.0 1.0.0 openrouter" try: if __package__ is not None: diff --git a/src/openrouter/components/__init__.py b/src/openrouter/components/__init__.py index e2409b0b..aa1609dc 100644 --- a/src/openrouter/components/__init__.py +++ b/src/openrouter/components/__init__.py @@ -2661,6 +2661,13 @@ ServiceUnavailableResponseErrorData, ServiceUnavailableResponseErrorDataTypedDict, ) + from .sessioncostitem import SessionCostItem, SessionCostItemTypedDict, TurnRange + from .sessioncostmeta import ( + SessionCostMeta, + SessionCostMetaTypedDict, + SessionCostMetaVersion, + ) + from .sessioncostresponse import SessionCostResponse, SessionCostResponseTypedDict from .shellcallitem import ( ShellCallItem, ShellCallItemAction, @@ -4953,6 +4960,13 @@ "ServerToolUseTypedDict", "ServiceUnavailableResponseErrorData", "ServiceUnavailableResponseErrorDataTypedDict", + "SessionCostItem", + "SessionCostItemTypedDict", + "SessionCostMeta", + "SessionCostMetaTypedDict", + "SessionCostMetaVersion", + "SessionCostResponse", + "SessionCostResponseTypedDict", "ShellCallItem", "ShellCallItemAction", "ShellCallItemActionTypedDict", @@ -5132,6 +5146,7 @@ "TriggerInputTokensTypedDict", "TriggerTypedDict", "Truncation", + "TurnRange", "Type", "TypeAdaptive", "TypeAdditionalTools", @@ -7303,6 +7318,14 @@ "ServerToolUseDetailsTypedDict": ".servertoolusedetails", "ServiceUnavailableResponseErrorData": ".serviceunavailableresponseerrordata", "ServiceUnavailableResponseErrorDataTypedDict": ".serviceunavailableresponseerrordata", + "SessionCostItem": ".sessioncostitem", + "SessionCostItemTypedDict": ".sessioncostitem", + "TurnRange": ".sessioncostitem", + "SessionCostMeta": ".sessioncostmeta", + "SessionCostMetaTypedDict": ".sessioncostmeta", + "SessionCostMetaVersion": ".sessioncostmeta", + "SessionCostResponse": ".sessioncostresponse", + "SessionCostResponseTypedDict": ".sessioncostresponse", "ShellCallItem": ".shellcallitem", "ShellCallItemAction": ".shellcallitem", "ShellCallItemActionTypedDict": ".shellcallitem", diff --git a/src/openrouter/components/sessioncostitem.py b/src/openrouter/components/sessioncostitem.py new file mode 100644 index 00000000..56f63156 --- /dev/null +++ b/src/openrouter/components/sessioncostitem.py @@ -0,0 +1,48 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from openrouter.types import BaseModel, UnrecognizedStr +from typing import Literal, Union +from typing_extensions import TypedDict + + +TurnRange = Union[ + Literal[ + "1-turn", + "2-9-turns", + "10-49-turns", + "50-plus-turns", + ], + UnrecognizedStr, +] +r"""Inclusive session turn-count range.""" + + +class SessionCostItemTypedDict(TypedDict): + app_name: str + r"""Published harness display label.""" + app_slug: str + r"""Stable public slug of the harness.""" + median_session_cost_usd: float + r"""Median USD spend per sampled session.""" + model_permaslug: str + r"""Exact model permaslug.""" + turn_range: TurnRange + r"""Inclusive session turn-count range.""" + + +class SessionCostItem(BaseModel): + app_name: str + r"""Published harness display label.""" + + app_slug: str + r"""Stable public slug of the harness.""" + + median_session_cost_usd: float + r"""Median USD spend per sampled session.""" + + model_permaslug: str + r"""Exact model permaslug.""" + + turn_range: TurnRange + r"""Inclusive session turn-count range.""" diff --git a/src/openrouter/components/sessioncostmeta.py b/src/openrouter/components/sessioncostmeta.py new file mode 100644 index 00000000..ce055e4b --- /dev/null +++ b/src/openrouter/components/sessioncostmeta.py @@ -0,0 +1,50 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from openrouter.types import BaseModel, Nullable, UNSET_SENTINEL +from pydantic import model_serializer +from typing import Literal +from typing_extensions import TypedDict + + +SessionCostMetaVersion = Literal["v1",] +r"""Dataset version.""" + + +class SessionCostMetaTypedDict(TypedDict): + as_of: str + r"""ISO-8601 timestamp when the response was generated.""" + version: SessionCostMetaVersion + r"""Dataset version.""" + window_days: Nullable[int] + r"""Number of days in the weekly session sample window, or null when no snapshot is published.""" + window_end_date: Nullable[str] + r"""UTC date of the final day in the session sample window, or null when no snapshot is published.""" + + +class SessionCostMeta(BaseModel): + as_of: str + r"""ISO-8601 timestamp when the response was generated.""" + + version: SessionCostMetaVersion + r"""Dataset version.""" + + window_days: Nullable[int] + r"""Number of days in the weekly session sample window, or null when no snapshot is published.""" + + window_end_date: Nullable[str] + r"""UTC date of the final day in the session sample window, or null when no snapshot is published.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + m[k] = val + + return m diff --git a/src/openrouter/components/sessioncostresponse.py b/src/openrouter/components/sessioncostresponse.py new file mode 100644 index 00000000..e105a7eb --- /dev/null +++ b/src/openrouter/components/sessioncostresponse.py @@ -0,0 +1,19 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .sessioncostitem import SessionCostItem, SessionCostItemTypedDict +from .sessioncostmeta import SessionCostMeta, SessionCostMetaTypedDict +from openrouter.types import BaseModel +from typing import List +from typing_extensions import TypedDict + + +class SessionCostResponseTypedDict(TypedDict): + data: List[SessionCostItemTypedDict] + meta: SessionCostMetaTypedDict + + +class SessionCostResponse(BaseModel): + data: List[SessionCostItem] + + meta: SessionCostMeta diff --git a/src/openrouter/datasets.py b/src/openrouter/datasets.py index a1e84756..d153d356 100644 --- a/src/openrouter/datasets.py +++ b/src/openrouter/datasets.py @@ -787,3 +787,378 @@ async def get_rankings_daily_async( ) raise errors.OpenRouterDefaultError("Unexpected response received", http_res) + + def get_session_cost( + self, + *, + http_referer: Optional[str] = None, + x_open_router_title: Optional[str] = None, + x_open_router_categories: Optional[str] = None, + app_slug: Optional[str] = None, + model: Optional[str] = None, + turn_range: Optional[operations.TurnRange] = None, + limit: Optional[int] = 100, + offset: OptionalNullable[int] = 0, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[operations.GetSessionCostResponse]: + r"""Cost per session by harness and model + + Returns weekly refreshed, aggregated cost-per-session cells for the published harnesses. + Sessions are never pooled across apps. Medians are of per-session USD spend, and + privacy-preserving aggregation never exposes clerk_user_id values or per-session rows. + + Filter by `app_slug`, `model`, or `turn_range`. Filtering by `model` alone works across apps + for harness-vs-harness comparison at a fixed model. Results refresh weekly and include the source snapshot + window in `meta`. + + :param http_referer: The app identifier should be your app's URL and is used as the primary identifier for rankings. + This is used to track API usage per application. + + :param x_open_router_title: The app display name allows you to customize how your app appears in OpenRouter's dashboard. + + :param x_open_router_categories: Comma-separated list of app categories (e.g. \"cli-agent,cloud-agent\"). Used for marketplace rankings. + + :param app_slug: Filter to one published harness slug. + :param model: Exact model permaslug filter. Works across all harness apps. + :param turn_range: Filter by the inclusive number of turns in a session. + :param limit: Maximum number of cells to return (1-500). Defaults to 100. + :param offset: Number of sorted cells to skip (0-5000). Defaults to 0. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = operations.GetSessionCostRequest( + http_referer=http_referer, + x_open_router_title=x_open_router_title, + x_open_router_categories=x_open_router_categories, + app_slug=app_slug, + model=model, + turn_range=turn_range, + limit=limit, + offset=offset, + ) + + req = self._build_request( + method="GET", + path="/datasets/session-cost", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=False, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + _globals=operations.GetSessionCostGlobals( + http_referer=self.sdk_configuration.globals.http_referer, + x_open_router_title=self.sdk_configuration.globals.x_open_router_title, + x_open_router_categories=self.sdk_configuration.globals.x_open_router_categories, + ), + security=self.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="getSessionCost", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, components.Security + ), + tags=["Datasets"], + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) + + def next_func() -> Optional[operations.GetSessionCostResponse]: + body = utils.unmarshal_json(http_res.text, Union[Dict[Any, Any], List[Any]]) + + offset = request.offset if isinstance(request.offset, int) else 0 + + if not http_res.text: + return None + results = JSONPath("$.data").parse(body) + if len(results) == 0 or len(results[0]) == 0: + return None + limit_ = request.limit if isinstance(request.limit, int) else 100 + if len(results[0]) < limit_: + return None + next_offset = offset + len(results[0]) + + return self.get_session_cost( + http_referer=http_referer, + x_open_router_title=x_open_router_title, + x_open_router_categories=x_open_router_categories, + app_slug=app_slug, + model=model, + turn_range=turn_range, + limit=limit, + offset=next_offset, + retries=retries, + server_url=server_url, + timeout_ms=timeout_ms, + http_headers=http_headers, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return operations.GetSessionCostResponse( + result=unmarshal_json_response( + components.SessionCostResponse, http_res + ), + next=next_func, + ) + if utils.match_response(http_res, "400", "application/json"): + response_data = unmarshal_json_response( + errors.BadRequestResponseErrorData, http_res + ) + raise errors.BadRequestResponseError(response_data, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.UnauthorizedResponseErrorData, http_res + ) + raise errors.UnauthorizedResponseError(response_data, http_res) + if utils.match_response(http_res, "429", "application/json"): + response_data = unmarshal_json_response( + errors.TooManyRequestsResponseErrorData, http_res + ) + raise errors.TooManyRequestsResponseError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.InternalServerResponseErrorData, http_res + ) + raise errors.InternalServerResponseError(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.OpenRouterDefaultError( + "API error occurred", http_res, http_res_text + ) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.OpenRouterDefaultError( + "API error occurred", http_res, http_res_text + ) + + raise errors.OpenRouterDefaultError("Unexpected response received", http_res) + + async def get_session_cost_async( + self, + *, + http_referer: Optional[str] = None, + x_open_router_title: Optional[str] = None, + x_open_router_categories: Optional[str] = None, + app_slug: Optional[str] = None, + model: Optional[str] = None, + turn_range: Optional[operations.TurnRange] = None, + limit: Optional[int] = 100, + offset: OptionalNullable[int] = 0, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[operations.GetSessionCostResponse]: + r"""Cost per session by harness and model + + Returns weekly refreshed, aggregated cost-per-session cells for the published harnesses. + Sessions are never pooled across apps. Medians are of per-session USD spend, and + privacy-preserving aggregation never exposes clerk_user_id values or per-session rows. + + Filter by `app_slug`, `model`, or `turn_range`. Filtering by `model` alone works across apps + for harness-vs-harness comparison at a fixed model. Results refresh weekly and include the source snapshot + window in `meta`. + + :param http_referer: The app identifier should be your app's URL and is used as the primary identifier for rankings. + This is used to track API usage per application. + + :param x_open_router_title: The app display name allows you to customize how your app appears in OpenRouter's dashboard. + + :param x_open_router_categories: Comma-separated list of app categories (e.g. \"cli-agent,cloud-agent\"). Used for marketplace rankings. + + :param app_slug: Filter to one published harness slug. + :param model: Exact model permaslug filter. Works across all harness apps. + :param turn_range: Filter by the inclusive number of turns in a session. + :param limit: Maximum number of cells to return (1-500). Defaults to 100. + :param offset: Number of sorted cells to skip (0-5000). Defaults to 0. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = operations.GetSessionCostRequest( + http_referer=http_referer, + x_open_router_title=x_open_router_title, + x_open_router_categories=x_open_router_categories, + app_slug=app_slug, + model=model, + turn_range=turn_range, + limit=limit, + offset=offset, + ) + + req = self._build_request_async( + method="GET", + path="/datasets/session-cost", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=False, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + _globals=operations.GetSessionCostGlobals( + http_referer=self.sdk_configuration.globals.http_referer, + x_open_router_title=self.sdk_configuration.globals.x_open_router_title, + x_open_router_categories=self.sdk_configuration.globals.x_open_router_categories, + ), + security=self.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="getSessionCost", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, components.Security + ), + tags=["Datasets"], + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) + + def next_func() -> Awaitable[Optional[operations.GetSessionCostResponse]]: + body = utils.unmarshal_json(http_res.text, Union[Dict[Any, Any], List[Any]]) + + async def empty_result(): + return None + + offset = request.offset if isinstance(request.offset, int) else 0 + + if not http_res.text: + return empty_result() + results = JSONPath("$.data").parse(body) + if len(results) == 0 or len(results[0]) == 0: + return empty_result() + limit_ = request.limit if isinstance(request.limit, int) else 100 + if len(results[0]) < limit_: + return empty_result() + next_offset = offset + len(results[0]) + + return self.get_session_cost_async( + http_referer=http_referer, + x_open_router_title=x_open_router_title, + x_open_router_categories=x_open_router_categories, + app_slug=app_slug, + model=model, + turn_range=turn_range, + limit=limit, + offset=next_offset, + retries=retries, + server_url=server_url, + timeout_ms=timeout_ms, + http_headers=http_headers, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return operations.GetSessionCostResponse( + result=unmarshal_json_response( + components.SessionCostResponse, http_res + ), + next=next_func, + ) + if utils.match_response(http_res, "400", "application/json"): + response_data = unmarshal_json_response( + errors.BadRequestResponseErrorData, http_res + ) + raise errors.BadRequestResponseError(response_data, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.UnauthorizedResponseErrorData, http_res + ) + raise errors.UnauthorizedResponseError(response_data, http_res) + if utils.match_response(http_res, "429", "application/json"): + response_data = unmarshal_json_response( + errors.TooManyRequestsResponseErrorData, http_res + ) + raise errors.TooManyRequestsResponseError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.InternalServerResponseErrorData, http_res + ) + raise errors.InternalServerResponseError(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.OpenRouterDefaultError( + "API error occurred", http_res, http_res_text + ) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.OpenRouterDefaultError( + "API error occurred", http_res, http_res_text + ) + + raise errors.OpenRouterDefaultError("Unexpected response received", http_res) diff --git a/src/openrouter/operations/__init__.py b/src/openrouter/operations/__init__.py index 50ebc6f5..76ee9167 100644 --- a/src/openrouter/operations/__init__.py +++ b/src/openrouter/operations/__init__.py @@ -445,6 +445,15 @@ GetScimGroupMappingRequest, GetScimGroupMappingRequestTypedDict, ) + from .getsessioncost import ( + GetSessionCostGlobals, + GetSessionCostGlobalsTypedDict, + GetSessionCostRequest, + GetSessionCostRequestTypedDict, + GetSessionCostResponse, + GetSessionCostResponseTypedDict, + TurnRange, + ) from .gettaskclassifications import ( GetTaskClassificationsGlobals, GetTaskClassificationsGlobalsTypedDict, @@ -1124,6 +1133,12 @@ "GetScimGroupMappingGlobalsTypedDict", "GetScimGroupMappingRequest", "GetScimGroupMappingRequestTypedDict", + "GetSessionCostGlobals", + "GetSessionCostGlobalsTypedDict", + "GetSessionCostRequest", + "GetSessionCostRequestTypedDict", + "GetSessionCostResponse", + "GetSessionCostResponseTypedDict", "GetTaskClassificationsGlobals", "GetTaskClassificationsGlobalsTypedDict", "GetTaskClassificationsRequest", @@ -1376,6 +1391,7 @@ "TimeRange", "TimeRangeTypedDict", "TimestampGranularities", + "TurnRange", "TypeImageURL", "TypeText", "UpdateBYOKKeyGlobals", @@ -1772,6 +1788,13 @@ "GetScimGroupMappingGlobalsTypedDict": ".getscimgroupmapping", "GetScimGroupMappingRequest": ".getscimgroupmapping", "GetScimGroupMappingRequestTypedDict": ".getscimgroupmapping", + "GetSessionCostGlobals": ".getsessioncost", + "GetSessionCostGlobalsTypedDict": ".getsessioncost", + "GetSessionCostRequest": ".getsessioncost", + "GetSessionCostRequestTypedDict": ".getsessioncost", + "GetSessionCostResponse": ".getsessioncost", + "GetSessionCostResponseTypedDict": ".getsessioncost", + "TurnRange": ".getsessioncost", "GetTaskClassificationsGlobals": ".gettaskclassifications", "GetTaskClassificationsGlobalsTypedDict": ".gettaskclassifications", "GetTaskClassificationsRequest": ".gettaskclassifications", diff --git a/src/openrouter/operations/getsessioncost.py b/src/openrouter/operations/getsessioncost.py new file mode 100644 index 00000000..7a5e3e1c --- /dev/null +++ b/src/openrouter/operations/getsessioncost.py @@ -0,0 +1,227 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from openrouter.components import sessioncostresponse as components_sessioncostresponse +from openrouter.types import ( + BaseModel, + Nullable, + OptionalNullable, + UNSET_SENTINEL, + UnrecognizedStr, +) +from openrouter.utils import FieldMetadata, HeaderMetadata, QueryParamMetadata +import pydantic +from pydantic import model_serializer +from typing import Awaitable, Callable, Literal, Optional, Union +from typing_extensions import Annotated, NotRequired, TypedDict + + +class GetSessionCostGlobalsTypedDict(TypedDict): + http_referer: NotRequired[str] + r"""The app identifier should be your app's URL and is used as the primary identifier for rankings. + This is used to track API usage per application. + + """ + x_open_router_title: NotRequired[str] + r"""The app display name allows you to customize how your app appears in OpenRouter's dashboard. + + """ + x_open_router_categories: NotRequired[str] + r"""Comma-separated list of app categories (e.g. \"cli-agent,cloud-agent\"). Used for marketplace rankings. + + """ + + +class GetSessionCostGlobals(BaseModel): + http_referer: Annotated[ + Optional[str], + pydantic.Field(alias="HTTP-Referer"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] = None + r"""The app identifier should be your app's URL and is used as the primary identifier for rankings. + This is used to track API usage per application. + + """ + + x_open_router_title: Annotated[ + Optional[str], + pydantic.Field(alias="X-OpenRouter-Title"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] = None + r"""The app display name allows you to customize how your app appears in OpenRouter's dashboard. + + """ + + x_open_router_categories: Annotated[ + Optional[str], + pydantic.Field(alias="X-OpenRouter-Categories"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] = None + r"""Comma-separated list of app categories (e.g. \"cli-agent,cloud-agent\"). Used for marketplace rankings. + + """ + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + ["HTTP-Referer", "X-OpenRouter-Title", "X-OpenRouter-Categories"] + ) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +TurnRange = Union[ + Literal[ + "1-turn", + "2-9-turns", + "10-49-turns", + "50-plus-turns", + ], + UnrecognizedStr, +] +r"""Filter by the inclusive number of turns in a session.""" + + +class GetSessionCostRequestTypedDict(TypedDict): + http_referer: NotRequired[str] + r"""The app identifier should be your app's URL and is used as the primary identifier for rankings. + This is used to track API usage per application. + + """ + x_open_router_title: NotRequired[str] + r"""The app display name allows you to customize how your app appears in OpenRouter's dashboard. + + """ + x_open_router_categories: NotRequired[str] + r"""Comma-separated list of app categories (e.g. \"cli-agent,cloud-agent\"). Used for marketplace rankings. + + """ + app_slug: NotRequired[str] + r"""Filter to one published harness slug.""" + model: NotRequired[str] + r"""Exact model permaslug filter. Works across all harness apps.""" + turn_range: NotRequired[TurnRange] + r"""Filter by the inclusive number of turns in a session.""" + limit: NotRequired[int] + r"""Maximum number of cells to return (1-500). Defaults to 100.""" + offset: NotRequired[Nullable[int]] + r"""Number of sorted cells to skip (0-5000). Defaults to 0.""" + + +class GetSessionCostRequest(BaseModel): + http_referer: Annotated[ + Optional[str], + pydantic.Field(alias="HTTP-Referer"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] = None + r"""The app identifier should be your app's URL and is used as the primary identifier for rankings. + This is used to track API usage per application. + + """ + + x_open_router_title: Annotated[ + Optional[str], + pydantic.Field(alias="X-OpenRouter-Title"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] = None + r"""The app display name allows you to customize how your app appears in OpenRouter's dashboard. + + """ + + x_open_router_categories: Annotated[ + Optional[str], + pydantic.Field(alias="X-OpenRouter-Categories"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] = None + r"""Comma-separated list of app categories (e.g. \"cli-agent,cloud-agent\"). Used for marketplace rankings. + + """ + + app_slug: Annotated[ + Optional[str], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""Filter to one published harness slug.""" + + model: Annotated[ + Optional[str], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""Exact model permaslug filter. Works across all harness apps.""" + + turn_range: Annotated[ + Optional[TurnRange], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""Filter by the inclusive number of turns in a session.""" + + limit: Annotated[ + Optional[int], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = 100 + r"""Maximum number of cells to return (1-500). Defaults to 100.""" + + offset: Annotated[ + OptionalNullable[int], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = 0 + r"""Number of sorted cells to skip (0-5000). Defaults to 0.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + [ + "HTTP-Referer", + "X-OpenRouter-Title", + "X-OpenRouter-Categories", + "app_slug", + "model", + "turn_range", + "limit", + "offset", + ] + ) + nullable_fields = set(["offset"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + is_nullable_and_explicitly_set = ( + k in nullable_fields + and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member + ) + + if val != UNSET_SENTINEL: + if ( + val is not None + or k not in optional_fields + or is_nullable_and_explicitly_set + ): + m[k] = val + + return m + + +class GetSessionCostResponseTypedDict(TypedDict): + result: components_sessioncostresponse.SessionCostResponseTypedDict + + +class GetSessionCostResponse(BaseModel): + next: Union[ + Callable[[], Optional[GetSessionCostResponse]], + Callable[[], Awaitable[Optional[GetSessionCostResponse]]], + ] + + result: components_sessioncostresponse.SessionCostResponse diff --git a/uv.lock b/uv.lock index 8563b94c..68f53232 100644 --- a/uv.lock +++ b/uv.lock @@ -213,7 +213,7 @@ wheels = [ [[package]] name = "openrouter" -version = "1.1.49" +version = "1.1.50" source = { editable = "." } dependencies = [ { name = "httpcore" }, From e24076a7d874a3f06b8ef83d850a144c9df2b554 Mon Sep 17 00:00:00 2001 From: "speakeasy-github[bot]" <128539517+speakeasy-github[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:08:20 +0000 Subject: [PATCH 2/2] empty commit to trigger [run-tests] workflow