runapi-core 0.8.2__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {runapi_core-0.8.2 → runapi_core-0.9.0}/PKG-INFO +3 -3
  2. {runapi_core-0.8.2 → runapi_core-0.9.0}/README.md +1 -1
  3. {runapi_core-0.8.2 → runapi_core-0.9.0}/pyproject.toml +1 -1
  4. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/__init__.py +2 -8
  5. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/contract_gen.py +218 -10
  6. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/models.py +8 -20
  7. runapi_core-0.9.0/src/runapi/core/version.py +1 -0
  8. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_models.py +29 -12
  9. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_resource.py +17 -33
  10. runapi_core-0.8.2/src/runapi/core/version.py +0 -1
  11. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/account.py +0 -0
  12. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/auth.py +0 -0
  13. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/config.py +0 -0
  14. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/constants.py +0 -0
  15. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/errors.py +0 -0
  16. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/files.py +0 -0
  17. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/http_client.py +0 -0
  18. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/multipart.py +0 -0
  19. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/options.py +0 -0
  20. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/polling.py +0 -0
  21. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/pricing.py +0 -0
  22. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/provider_client.py +0 -0
  23. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/py.typed +0 -0
  24. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/resource.py +0 -0
  25. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/response.py +0 -0
  26. {runapi_core-0.8.2 → runapi_core-0.9.0}/src/runapi/core/uploads.py +0 -0
  27. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_account.py +0 -0
  28. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_auth.py +0 -0
  29. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_config.py +0 -0
  30. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_errors.py +0 -0
  31. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_files.py +0 -0
  32. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_http_client.py +0 -0
  33. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_polling.py +0 -0
  34. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_pricing.py +0 -0
  35. {runapi_core-0.8.2 → runapi_core-0.9.0}/tests/test_uploads.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: runapi-core
3
- Version: 0.8.2
3
+ Version: 0.9.0
4
4
  Summary: RunAPI core SDK for JavaScript, Python, Ruby, Go, Java, and PHP
5
5
  Project-URL: Homepage, https://runapi.ai/models
6
6
  Project-URL: Documentation, https://runapi.ai/docs/resources/sdks
7
7
  Project-URL: Source, https://github.com/runapi-ai/core-sdk
8
8
  Project-URL: Issues, https://github.com/runapi-ai/core-sdk/issues
9
9
  Project-URL: Changelog, https://github.com/runapi-ai/core-sdk/blob/main/CHANGELOG.md
10
- Project-URL: Release Notes, https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.8.2
10
+ Project-URL: Release Notes, https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.9.0
11
11
  Author-email: RunAPI <contact@runapi.ai>
12
12
  License-Expression: Apache-2.0
13
13
  Keywords: api,core,golang,gradle,java,maven,pypi,python,ruby,runapi,runapi-ai,sdk,typescript
@@ -134,7 +134,7 @@ quote = client.pricing.create_quote(
134
134
  )
135
135
  ```
136
136
 
137
- Task responses expose persisted billing facts at `response.billing`: `reservation`, `settlement`, and `refund` are typed objects when recorded and `None` when the historical fact is absent. These facts describe that Task and are not recalculated from the current schedule.
137
+ Completed Task query and webhook envelopes expose `usage.cost` as a float USD amount for that Task. Create, processing, and failed envelopes omit `usage`. The value is the recorded settlement for that Task and is never recalculated from the current price schedule.
138
138
 
139
139
  ## License
140
140
 
@@ -117,7 +117,7 @@ quote = client.pricing.create_quote(
117
117
  )
118
118
  ```
119
119
 
120
- Task responses expose persisted billing facts at `response.billing`: `reservation`, `settlement`, and `refund` are typed objects when recorded and `None` when the historical fact is absent. These facts describe that Task and are not recalculated from the current schedule.
120
+ Completed Task query and webhook envelopes expose `usage.cost` as a float USD amount for that Task. Create, processing, and failed envelopes omit `usage`. The value is the recorded settlement for that Task and is never recalculated from the current price schedule.
121
121
 
122
122
  ## License
123
123
 
@@ -19,7 +19,7 @@ Documentation = "https://runapi.ai/docs/resources/sdks"
19
19
  Source = "https://github.com/runapi-ai/core-sdk"
20
20
  Issues = "https://github.com/runapi-ai/core-sdk/issues"
21
21
  Changelog = "https://github.com/runapi-ai/core-sdk/blob/main/CHANGELOG.md"
22
- "Release Notes" = "https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.8.2"
22
+ "Release Notes" = "https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.9.0"
23
23
 
24
24
  [tool.hatch.version]
25
25
  path = "src/runapi/core/version.py"
@@ -29,14 +29,11 @@ from .files import DeletedFile, FileList, FileObject, FilesClient, UploadRespons
29
29
  from .http_client import HttpClient
30
30
  from .models import (
31
31
  BaseModel,
32
- BillingRefund,
33
- BillingReservation,
34
- BillingSettlement,
35
32
  DynamicModel,
36
- TaskBillingFacts,
37
33
  TaskResult,
38
34
  TaskResultResponse,
39
35
  TaskResponse,
36
+ TaskUsage,
40
37
  optional,
41
38
  required,
42
39
  )
@@ -86,10 +83,7 @@ __all__ = [
86
83
  "TaskResponse",
87
84
  "TaskResult",
88
85
  "TaskResultResponse",
89
- "BillingReservation",
90
- "BillingSettlement",
91
- "BillingRefund",
92
- "TaskBillingFacts",
86
+ "TaskUsage",
93
87
  "required",
94
88
  "optional",
95
89
  "MultipartBody",
@@ -1686,6 +1686,12 @@ CONTRACT = {
1686
1686
  }
1687
1687
  }
1688
1688
  },
1689
+ "suno/audio-exports": {
1690
+ "models": [],
1691
+ "fields_by_model": {
1692
+ "_": {}
1693
+ }
1694
+ },
1689
1695
  "suno/blend-lyrics": {
1690
1696
  "models": [],
1691
1697
  "fields_by_model": {
@@ -1711,7 +1717,7 @@ CONTRACT = {
1711
1717
  }
1712
1718
  },
1713
1719
  "suno/cover-audio": {
1714
- "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
1720
+ "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5", "suno-v6", "suno-v6-mini", "suno-v6-wild"],
1715
1721
  "fields_by_model": {
1716
1722
  "suno-v4": {
1717
1723
  "persona_type": {
@@ -1778,11 +1784,44 @@ CONTRACT = {
1778
1784
  "vocal_mode": {
1779
1785
  "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
1780
1786
  }
1787
+ },
1788
+ "suno-v6": {
1789
+ "persona_type": {
1790
+ "enum": ["style", "voice"]
1791
+ },
1792
+ "vocal_gender": {
1793
+ "enum": ["male", "female"]
1794
+ },
1795
+ "vocal_mode": {
1796
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
1797
+ }
1798
+ },
1799
+ "suno-v6-mini": {
1800
+ "persona_type": {
1801
+ "enum": ["style", "voice"]
1802
+ },
1803
+ "vocal_gender": {
1804
+ "enum": ["male", "female"]
1805
+ },
1806
+ "vocal_mode": {
1807
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
1808
+ }
1809
+ },
1810
+ "suno-v6-wild": {
1811
+ "persona_type": {
1812
+ "enum": ["style", "voice"]
1813
+ },
1814
+ "vocal_gender": {
1815
+ "enum": ["male", "female"]
1816
+ },
1817
+ "vocal_mode": {
1818
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
1819
+ }
1781
1820
  }
1782
1821
  }
1783
1822
  },
1784
1823
  "suno/create-mashup": {
1785
- "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
1824
+ "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5", "suno-v6", "suno-v6-mini", "suno-v6-wild"],
1786
1825
  "fields_by_model": {
1787
1826
  "suno-v4": {
1788
1827
  "persona_type": {
@@ -1849,11 +1888,44 @@ CONTRACT = {
1849
1888
  "vocal_mode": {
1850
1889
  "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
1851
1890
  }
1891
+ },
1892
+ "suno-v6": {
1893
+ "persona_type": {
1894
+ "enum": ["style", "voice"]
1895
+ },
1896
+ "vocal_gender": {
1897
+ "enum": ["male", "female"]
1898
+ },
1899
+ "vocal_mode": {
1900
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
1901
+ }
1902
+ },
1903
+ "suno-v6-mini": {
1904
+ "persona_type": {
1905
+ "enum": ["style", "voice"]
1906
+ },
1907
+ "vocal_gender": {
1908
+ "enum": ["male", "female"]
1909
+ },
1910
+ "vocal_mode": {
1911
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
1912
+ }
1913
+ },
1914
+ "suno-v6-wild": {
1915
+ "persona_type": {
1916
+ "enum": ["style", "voice"]
1917
+ },
1918
+ "vocal_gender": {
1919
+ "enum": ["male", "female"]
1920
+ },
1921
+ "vocal_mode": {
1922
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
1923
+ }
1852
1924
  }
1853
1925
  }
1854
1926
  },
1855
1927
  "suno/extend-music": {
1856
- "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
1928
+ "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5", "suno-v6", "suno-v6-mini", "suno-v6-wild"],
1857
1929
  "fields_by_model": {
1858
1930
  "suno-v4": {
1859
1931
  "parameter_mode": {
@@ -1920,6 +1992,39 @@ CONTRACT = {
1920
1992
  "vocal_gender": {
1921
1993
  "enum": ["male", "female"]
1922
1994
  }
1995
+ },
1996
+ "suno-v6": {
1997
+ "parameter_mode": {
1998
+ "enum": ["source", "custom"]
1999
+ },
2000
+ "persona_type": {
2001
+ "enum": ["style", "voice"]
2002
+ },
2003
+ "vocal_gender": {
2004
+ "enum": ["male", "female"]
2005
+ }
2006
+ },
2007
+ "suno-v6-mini": {
2008
+ "parameter_mode": {
2009
+ "enum": ["source", "custom"]
2010
+ },
2011
+ "persona_type": {
2012
+ "enum": ["style", "voice"]
2013
+ },
2014
+ "vocal_gender": {
2015
+ "enum": ["male", "female"]
2016
+ }
2017
+ },
2018
+ "suno-v6-wild": {
2019
+ "parameter_mode": {
2020
+ "enum": ["source", "custom"]
2021
+ },
2022
+ "persona_type": {
2023
+ "enum": ["style", "voice"]
2024
+ },
2025
+ "vocal_gender": {
2026
+ "enum": ["male", "female"]
2027
+ }
1923
2028
  }
1924
2029
  }
1925
2030
  },
@@ -1973,6 +2078,28 @@ CONTRACT = {
1973
2078
  "suno-v5.5": {}
1974
2079
  }
1975
2080
  },
2081
+ "suno/music-from-sample": {
2082
+ "models": ["suno-v4", "suno-v4.5", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
2083
+ "fields_by_model": {
2084
+ "suno-v4": {},
2085
+ "suno-v4.5": {},
2086
+ "suno-v4.5-plus": {},
2087
+ "suno-v5": {},
2088
+ "suno-v5.5": {}
2089
+ }
2090
+ },
2091
+ "suno/music-visualizations": {
2092
+ "models": [],
2093
+ "fields_by_model": {
2094
+ "_": {}
2095
+ }
2096
+ },
2097
+ "suno/personas": {
2098
+ "models": [],
2099
+ "fields_by_model": {
2100
+ "_": {}
2101
+ }
2102
+ },
1976
2103
  "suno/regenerate-validation-phrase": {
1977
2104
  "models": [],
1978
2105
  "fields_by_model": {
@@ -1982,11 +2109,31 @@ CONTRACT = {
1982
2109
  "suno/remaster-audio": {
1983
2110
  "models": ["suno-v4", "suno-v4.5", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
1984
2111
  "fields_by_model": {
1985
- "suno-v4": {},
1986
- "suno-v4.5": {},
1987
- "suno-v4.5-plus": {},
1988
- "suno-v5": {},
1989
- "suno-v5.5": {}
2112
+ "suno-v4": {
2113
+ "variation_category": {
2114
+ "enum": ["subtle", "normal", "high"]
2115
+ }
2116
+ },
2117
+ "suno-v4.5": {
2118
+ "variation_category": {
2119
+ "enum": ["subtle", "normal", "high"]
2120
+ }
2121
+ },
2122
+ "suno-v4.5-plus": {
2123
+ "variation_category": {
2124
+ "enum": ["subtle", "normal", "high"]
2125
+ }
2126
+ },
2127
+ "suno-v5": {
2128
+ "variation_category": {
2129
+ "enum": ["subtle", "normal", "high"]
2130
+ }
2131
+ },
2132
+ "suno-v5.5": {
2133
+ "variation_category": {
2134
+ "enum": ["subtle", "normal", "high"]
2135
+ }
2136
+ }
1990
2137
  }
1991
2138
  },
1992
2139
  "suno/replace-section": {
@@ -1994,7 +2141,7 @@ CONTRACT = {
1994
2141
  "fields_by_model": {
1995
2142
  "_": {
1996
2143
  "model": {
1997
- "enum": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5"]
2144
+ "enum": ["suno-v6", "suno-v6-wild", "suno-v6-mini", "suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5"]
1998
2145
  }
1999
2146
  }
2000
2147
  }
@@ -2022,8 +2169,14 @@ CONTRACT = {
2022
2169
  "suno-v5.5": {}
2023
2170
  }
2024
2171
  },
2172
+ "suno/style-expansions": {
2173
+ "models": [],
2174
+ "fields_by_model": {
2175
+ "_": {}
2176
+ }
2177
+ },
2025
2178
  "suno/text-to-music": {
2026
- "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
2179
+ "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5", "suno-v6", "suno-v6-mini", "suno-v6-wild"],
2027
2180
  "fields_by_model": {
2028
2181
  "suno-v4": {
2029
2182
  "persona_type": {
@@ -2090,6 +2243,39 @@ CONTRACT = {
2090
2243
  "vocal_mode": {
2091
2244
  "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
2092
2245
  }
2246
+ },
2247
+ "suno-v6": {
2248
+ "persona_type": {
2249
+ "enum": ["style", "voice"]
2250
+ },
2251
+ "vocal_gender": {
2252
+ "enum": ["male", "female"]
2253
+ },
2254
+ "vocal_mode": {
2255
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
2256
+ }
2257
+ },
2258
+ "suno-v6-mini": {
2259
+ "persona_type": {
2260
+ "enum": ["style", "voice"]
2261
+ },
2262
+ "vocal_gender": {
2263
+ "enum": ["male", "female"]
2264
+ },
2265
+ "vocal_mode": {
2266
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
2267
+ }
2268
+ },
2269
+ "suno-v6-wild": {
2270
+ "persona_type": {
2271
+ "enum": ["style", "voice"]
2272
+ },
2273
+ "vocal_gender": {
2274
+ "enum": ["male", "female"]
2275
+ },
2276
+ "vocal_mode": {
2277
+ "enum": ["auto_lyrics", "exact_lyrics", "instrumental"]
2278
+ }
2093
2279
  }
2094
2280
  }
2095
2281
  },
@@ -2108,6 +2294,12 @@ CONTRACT = {
2108
2294
  }
2109
2295
  }
2110
2296
  },
2297
+ "suno/timestamped-lyrics": {
2298
+ "models": [],
2299
+ "fields_by_model": {
2300
+ "_": {}
2301
+ }
2302
+ },
2111
2303
  "suno/visualize-music": {
2112
2304
  "models": [],
2113
2305
  "fields_by_model": {
@@ -2124,6 +2316,12 @@ CONTRACT = {
2124
2316
  }
2125
2317
  }
2126
2318
  },
2319
+ "suno/voices": {
2320
+ "models": [],
2321
+ "fields_by_model": {
2322
+ "_": {}
2323
+ }
2324
+ },
2127
2325
  "topaz/upscale-image": {
2128
2326
  "models": ["topaz-upscale-image"],
2129
2327
  "fields_by_model": {
@@ -2144,6 +2342,16 @@ CONTRACT = {
2144
2342
  }
2145
2343
  }
2146
2344
  },
2345
+ "typesafe/system-one": {
2346
+ "models": ["jev-latest"],
2347
+ "fields_by_model": {
2348
+ "jev-latest": {
2349
+ "model": {
2350
+ "enum": ["jev-latest"]
2351
+ }
2352
+ }
2353
+ }
2354
+ },
2147
2355
  "veo-3-1/extend-video": {
2148
2356
  "models": [],
2149
2357
  "fields_by_model": {
@@ -263,7 +263,7 @@ class TaskResponse(BaseModel):
263
263
  id = optional(str)
264
264
  status = optional(str)
265
265
  error = optional(str)
266
- billing = optional(lambda: TaskBillingFacts)
266
+ usage = optional(lambda: TaskUsage)
267
267
 
268
268
 
269
269
  class TaskResultResponse(BaseModel):
@@ -275,28 +275,16 @@ class TaskResultResponse(BaseModel):
275
275
  body = optional()
276
276
 
277
277
 
278
- class TaskResult(TaskResponse):
278
+ class TaskResult(BaseModel):
279
279
  """The account-scoped Task Result resource returned from an opaque Location."""
280
280
 
281
+ Status = TaskResponse.Status
282
+
281
283
  id = required(str)
282
- status = required(str, enum=lambda: TaskResponse.Status.ALL)
284
+ status = required(str, enum=lambda: TaskResult.Status.ALL)
285
+ error = optional(str)
283
286
  response = optional(lambda: TaskResultResponse)
284
287
 
285
288
 
286
- class BillingReservation(BaseModel):
287
- amount_cents = required(int)
288
-
289
-
290
- class BillingSettlement(BaseModel):
291
- charged_amount_cents = required(int)
292
- amount_micro_cents = required(int)
293
-
294
-
295
- class BillingRefund(BaseModel):
296
- refunded_at = required(str)
297
-
298
-
299
- class TaskBillingFacts(BaseModel):
300
- reservation = optional(BillingReservation)
301
- settlement = optional(BillingSettlement)
302
- refund = optional(BillingRefund)
289
+ class TaskUsage(BaseModel):
290
+ cost = required(float)
@@ -0,0 +1 @@
1
+ __version__ = "0.9.0"
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
 
3
- from runapi.core import BaseModel, DynamicModel, TaskBillingFacts, TaskResponse, optional, required
3
+ from runapi.core import BaseModel, DynamicModel, TaskResult, TaskResponse, TaskUsage, optional, required
4
4
  from runapi.core.errors import ValidationError
5
5
 
6
6
 
@@ -34,7 +34,7 @@ def test_required_field_missing_raises():
34
34
 
35
35
 
36
36
  def test_item_and_dot_access():
37
- model = Sample({"id": "abc", "status": "completed"})
37
+ model = Sample({"id": "abc", "status": "completed", "usage": {"cost": 0.05}})
38
38
  assert model["id"] == "abc"
39
39
  assert model.status == "completed"
40
40
 
@@ -45,8 +45,7 @@ def test_coerces_nested_hashes_and_arrays():
45
45
  "id": "abc",
46
46
  "meta": {"count": 2},
47
47
  "items": [{"name": "first"}],
48
- "extra_field": {"nested": True},
49
- }
48
+ "extra_field": {"nested": True}}
50
49
  )
51
50
  assert isinstance(model.meta, DynamicModel)
52
51
  assert model.meta.count == 2
@@ -70,8 +69,8 @@ def test_to_dict_serializes_recursively():
70
69
 
71
70
 
72
71
  def test_equality_with_dict():
73
- model = Sample({"id": "abc", "status": "completed"})
74
- assert model == {"id": "abc", "status": "completed"}
72
+ model = Sample({"id": "abc", "status": "completed", "usage": {"cost": 0.05}})
73
+ assert model == {"id": "abc", "status": "completed", "usage": {"cost": 0.05}}
75
74
 
76
75
 
77
76
  def test_optional_absent_field_is_none():
@@ -119,14 +118,32 @@ def test_response_headers_stay_outside_serialized_body():
119
118
  assert model.to_dict() == {"id": "abc"}
120
119
 
121
120
 
122
- def test_task_response_coerces_typed_billing_facts_and_preserves_unknown_fields():
121
+ def test_task_response_coerces_typed_usage_and_preserves_unknown_fields():
123
122
  response = TaskResponse(
124
123
  {
125
124
  "id": "task_1",
126
- "billing": {"reservation": {"amount_cents": 5}, "future_fact": "retained"},
127
- }
125
+ "status": "completed",
126
+ "usage": {"cost": 0.05, "future_fact": "retained"}}
128
127
  )
129
128
 
130
- assert isinstance(response.billing, TaskBillingFacts)
131
- assert response.billing.reservation.amount_cents == 5
132
- assert response.billing.future_fact == "retained"
129
+ assert isinstance(response.usage, TaskUsage)
130
+ assert response.usage.cost == 0.05
131
+ assert response.usage.future_fact == "retained"
132
+
133
+
134
+ def test_task_result_preserves_public_status_constants_without_task_usage():
135
+ assert [
136
+ TaskResult.Status.PENDING,
137
+ TaskResult.Status.PROCESSING,
138
+ TaskResult.Status.COMPLETED,
139
+ TaskResult.Status.FAILED,
140
+ ] == ["pending", "processing", "completed", "failed"]
141
+
142
+ result = TaskResult({"id": "task_1", "status": TaskResult.Status.COMPLETED})
143
+ assert result.status == "completed"
144
+ assert "usage" not in result.to_dict()
145
+ with pytest.raises(AttributeError):
146
+ _ = result.usage
147
+
148
+ with pytest.raises(ValidationError, match="Invalid status"):
149
+ TaskResult({"id": "task_1", "status": "unknown"})
@@ -91,10 +91,8 @@ INT_SCHEMA = {
91
91
  "fields_by_model": {
92
92
  "m": {
93
93
  "duration_int": {"type": "integer", "min": 4, "max": 12},
94
- "tolerance": {"type": "integer"},
95
- }
96
- },
97
- }
94
+ "tolerance": {"type": "integer"}}
95
+ }}
98
96
 
99
97
 
100
98
  def _run_validate(params):
@@ -130,10 +128,8 @@ def test_validate_functional_action_uses_underscore_fields():
130
128
  "fields_by_model": {
131
129
  "_": {
132
130
  "prompt": {"required": True},
133
- "mode": {"enum": ["fast", "quality"]},
134
- }
135
- },
136
- }
131
+ "mode": {"enum": ["fast", "quality"]}}
132
+ }}
137
133
  resource._validate_contract(schema, {"prompt": "hello", "mode": "fast"})
138
134
  with pytest.raises(ValidationError, match="prompt is required"):
139
135
  resource._validate_contract(schema, {"mode": "fast"})
@@ -164,10 +160,8 @@ def test_validate_array_item_count_constraints():
164
160
  "models": ["m"],
165
161
  "fields_by_model": {
166
162
  "m": {
167
- "reference_image_urls": {"min_items": 1, "max_items": 3},
168
- }
169
- },
170
- }
163
+ "reference_image_urls": {"min_items": 1, "max_items": 3}}
164
+ }}
171
165
  resource = SampleResource(FakeHttp())
172
166
 
173
167
  with pytest.raises(ValidationError, match="^reference_image_urls must be an array$"):
@@ -187,8 +181,7 @@ def test_validate_contract_runs_rules_before_field_requirements():
187
181
  schema = {
188
182
  "models": ["m"],
189
183
  "rules": [{"when": {"model": "m"}, "forbidden": ["source_task_id"]}],
190
- "fields_by_model": {"m": {"source_image_urls": {"required": True}}},
191
- }
184
+ "fields_by_model": {"m": {"source_image_urls": {"required": True}}}}
192
185
  resource = SampleResource(FakeHttp())
193
186
  with pytest.raises(ValidationError, match="source_task_id is not allowed when model is m"):
194
187
  resource._validate_contract(schema, {"model": "m", "source_task_id": "src_1"})
@@ -196,7 +189,7 @@ def test_validate_contract_runs_rules_before_field_requirements():
196
189
 
197
190
  def test_poll_recoerces_to_completed_class():
198
191
  resource = SampleResource(FakeHttp())
199
- response = TaskResponse({"id": "1", "status": "completed", "images": [{"url": "u"}]})
192
+ response = TaskResponse({"id": "1", "status": "completed", "usage": {"cost": 0.05}, "images": [{"url": "u"}]})
200
193
  result = resource._poll_until_complete(lambda: response, PollingOptions(poll_interval=0, max_wait=1))
201
194
  assert isinstance(result, CompletedResponse)
202
195
  assert result.images[0].url == "u"
@@ -215,14 +208,12 @@ def test_run_hybrid_uses_opaque_location_and_decodes_completed_json(monkeypatch)
215
208
  ApiResponse(
216
209
  {
217
210
  "id": "task_1",
218
- "status": "completed",
211
+ "status": "completed", "usage": {"cost": 0.05},
219
212
  "response": {
220
213
  "status": 200,
221
214
  "content_type": "application/json",
222
215
  "headers": {"Location": "https://runapi.ai/result"},
223
- "body": {"id": "task_1", "status": "completed", "prompts": ["short prompt"]},
224
- },
225
- }
216
+ "body": {"id": "task_1", "status": "completed", "usage": {"cost": 0.05}, "prompts": ["short prompt"]}}}
226
217
  ),
227
218
  )
228
219
 
@@ -234,8 +225,7 @@ def test_run_hybrid_uses_opaque_location_and_decodes_completed_json(monkeypatch)
234
225
  assert [call[:2] for call in http.calls] == [
235
226
  ("post", "/shorten"),
236
227
  ("get", "https://runapi.ai/api/v1/tasks/task_1"),
237
- ("get", "https://runapi.ai/api/v1/tasks/task_1"),
238
- ]
228
+ ("get", "https://runapi.ai/api/v1/tasks/task_1")]
239
229
  assert slept == [3.0, 2.0]
240
230
 
241
231
 
@@ -248,7 +238,7 @@ def test_run_hybrid_honors_http_date_retry_after(monkeypatch):
248
238
  http = FakeHttp(
249
239
  ApiResponse({"id": "task_1", "status": "processing"}, {"Location": "/api/v1/tasks/task_1", "Retry-After": initial_retry_after}, status_code=202),
250
240
  ApiResponse({"id": "task_1", "status": "processing"}, {"Retry-After": polling_retry_after}),
251
- ApiResponse({"id": "task_1", "status": "completed", "response": {"status": 200, "content_type": "application/json", "headers": {}, "body": {"id": "task_1", "status": "completed", "prompts": ["short prompt"]}}}),
241
+ ApiResponse({"id": "task_1", "status": "completed", "usage": {"cost": 0.05}, "response": {"status": 200, "content_type": "application/json", "headers": {}, "body": {"id": "task_1", "status": "completed", "usage": {"cost": 0.05}, "prompts": ["short prompt"]}}}),
252
242
  )
253
243
 
254
244
  HybridResource(http)._run_hybrid("post", "/shorten", {"prompt": "long"})
@@ -273,14 +263,12 @@ def test_run_hybrid_follows_a_real_http_stub_and_reuses_generated_key(monkeypatc
273
263
  200,
274
264
  json={
275
265
  "id": "task_1",
276
- "status": "completed",
266
+ "status": "completed", "usage": {"cost": 0.05},
277
267
  "response": {
278
268
  "status": 200,
279
269
  "content_type": "application/json",
280
270
  "headers": {},
281
- "body": {"id": "task_1", "status": "completed", "prompts": ["short prompt"]},
282
- },
283
- },
271
+ "body": {"id": "task_1", "status": "completed", "usage": {"cost": 0.05}, "prompts": ["short prompt"]}}},
284
272
  )
285
273
 
286
274
  http = HttpClient(
@@ -383,14 +371,12 @@ def test_subscribe_returns_text_srt_and_vtt_without_coercing_to_bytes(monkeypatc
383
371
  ApiResponse(
384
372
  {
385
373
  "id": "task_text",
386
- "status": "completed",
374
+ "status": "completed", "usage": {"cost": 0.05},
387
375
  "response": {
388
376
  "status": 200,
389
377
  "content_type": "text/vtt; charset=utf-8",
390
378
  "headers": {},
391
- "body": "WEBVTT\n\n00:00.000 --> 00:01.000\nHello",
392
- },
393
- }
379
+ "body": "WEBVTT\n\n00:00.000 --> 00:01.000\nHello"}}
394
380
  )
395
381
  )
396
382
 
@@ -410,9 +396,7 @@ def test_subscribe_maps_failed_task_checkpoint_to_public_error(monkeypatch):
410
396
  "status": 422,
411
397
  "content_type": "application/json",
412
398
  "headers": {},
413
- "body": {"error": "Prompt rejected"},
414
- },
415
- }
399
+ "body": {"error": "Prompt rejected"}}}
416
400
  )
417
401
  )
418
402
 
@@ -1 +0,0 @@
1
- __version__ = "0.8.2"