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" },