runapi-core 0.8.3__tar.gz → 0.9.1__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 (36) hide show
  1. {runapi_core-0.8.3 → runapi_core-0.9.1}/PKG-INFO +3 -3
  2. {runapi_core-0.8.3 → runapi_core-0.9.1}/README.md +1 -1
  3. {runapi_core-0.8.3 → runapi_core-0.9.1}/pyproject.toml +1 -1
  4. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/__init__.py +2 -8
  5. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/contract_gen.py +41 -5
  6. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/models.py +8 -20
  7. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/resource.py +15 -5
  8. runapi_core-0.9.1/src/runapi/core/version.py +1 -0
  9. runapi_core-0.9.1/tests/test_contract_rule_messages.py +28 -0
  10. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_models.py +29 -12
  11. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_resource.py +18 -34
  12. runapi_core-0.8.3/src/runapi/core/version.py +0 -1
  13. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/account.py +0 -0
  14. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/auth.py +0 -0
  15. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/config.py +0 -0
  16. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/constants.py +0 -0
  17. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/errors.py +0 -0
  18. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/files.py +0 -0
  19. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/http_client.py +0 -0
  20. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/multipart.py +0 -0
  21. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/options.py +0 -0
  22. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/polling.py +0 -0
  23. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/pricing.py +0 -0
  24. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/provider_client.py +0 -0
  25. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/py.typed +0 -0
  26. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/response.py +0 -0
  27. {runapi_core-0.8.3 → runapi_core-0.9.1}/src/runapi/core/uploads.py +0 -0
  28. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_account.py +0 -0
  29. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_auth.py +0 -0
  30. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_config.py +0 -0
  31. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_errors.py +0 -0
  32. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_files.py +0 -0
  33. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_http_client.py +0 -0
  34. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_polling.py +0 -0
  35. {runapi_core-0.8.3 → runapi_core-0.9.1}/tests/test_pricing.py +0 -0
  36. {runapi_core-0.8.3 → runapi_core-0.9.1}/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.3
3
+ Version: 0.9.1
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.3
10
+ Project-URL: Release Notes, https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.9.1
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.3"
22
+ "Release Notes" = "https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.9.1"
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",
@@ -2109,11 +2109,31 @@ CONTRACT = {
2109
2109
  "suno/remaster-audio": {
2110
2110
  "models": ["suno-v4", "suno-v4.5", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
2111
2111
  "fields_by_model": {
2112
- "suno-v4": {},
2113
- "suno-v4.5": {},
2114
- "suno-v4.5-plus": {},
2115
- "suno-v5": {},
2116
- "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
+ }
2117
2137
  }
2118
2138
  },
2119
2139
  "suno/replace-section": {
@@ -2322,6 +2342,16 @@ CONTRACT = {
2322
2342
  }
2323
2343
  }
2324
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
+ },
2325
2355
  "veo-3-1/extend-video": {
2326
2356
  "models": [],
2327
2357
  "fields_by_model": {
@@ -2429,6 +2459,9 @@ CONTRACT = {
2429
2459
  }
2430
2460
  },
2431
2461
  "wan-2.5-image-to-video": {
2462
+ "duration_seconds": {
2463
+ "enum": [5, 10]
2464
+ },
2432
2465
  "output_resolution": {
2433
2466
  "enum": ["720p", "1080p"]
2434
2467
  }
@@ -2493,6 +2526,9 @@ CONTRACT = {
2493
2526
  }
2494
2527
  },
2495
2528
  "wan-2.5-text-to-video": {
2529
+ "duration_seconds": {
2530
+ "enum": [5, 10]
2531
+ },
2496
2532
  "output_resolution": {
2497
2533
  "enum": ["720p", "1080p"]
2498
2534
  }
@@ -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)
@@ -227,7 +227,7 @@ class Resource:
227
227
 
228
228
  enum = rules.get("enum")
229
229
  if enum is not None and not self._enum_allowed(enum, value):
230
- raise ValidationError(f"{field} must be one of: {', '.join(str(option) for option in enum)}")
230
+ raise ValidationError(f"{field} must be one of: {', '.join(self._message_value(option) for option in enum)}")
231
231
 
232
232
  if rules.get("type") == "integer":
233
233
  self._validate_integer(field, value, rules)
@@ -318,8 +318,9 @@ class Resource:
318
318
  ):
319
319
  return
320
320
 
321
+ # Sorted so the message matches every other SDK; Go maps have no declaration order.
321
322
  context = " and ".join(
322
- self._rule_condition_label(key, val) for key, val in conditions.items()
323
+ self._rule_condition_label(key, conditions[key]) for key in sorted(conditions)
323
324
  )
324
325
  qualifier = f" when {context}" if context else ""
325
326
 
@@ -335,12 +336,14 @@ class Resource:
335
336
  if self._field_present(params, field):
336
337
  raise ValidationError(f"{field} is not allowed{qualifier}")
337
338
 
338
- for field, allowed in (rule.get("enum") or {}).items():
339
+ narrowed = rule.get("enum") or {}
340
+ for field in sorted(narrowed):
341
+ allowed = narrowed[field]
339
342
  if not self._field_present(params, field):
340
343
  continue
341
344
  if any(str(candidate) == str(params[field]) for candidate in allowed):
342
345
  continue
343
- joined = ", ".join(str(candidate) for candidate in allowed)
346
+ joined = ", ".join(self._message_value(candidate) for candidate in allowed)
344
347
  raise ValidationError(f"{field} must be one of: {joined}{qualifier}")
345
348
 
346
349
  def _rule_condition_met(self, params: Dict[str, Any], field: str, condition: Any) -> bool:
@@ -360,7 +363,14 @@ class Resource:
360
363
  def _rule_condition_label(self, field: str, condition: Any) -> str:
361
364
  if self._is_presence_condition(condition):
362
365
  return f"{field} is present" if condition["present"] is True else f"{field} is absent"
363
- return f"{field} is {condition}"
366
+ return f"{field} is {self._message_value(condition)}"
367
+
368
+ @staticmethod
369
+ def _message_value(value: Any) -> str:
370
+ """Render booleans as JSON does (``false``) to match the other SDKs."""
371
+ if isinstance(value, bool):
372
+ return "true" if value else "false"
373
+ return str(value)
364
374
 
365
375
  @classmethod
366
376
  def _field_present(cls, params: Dict[str, Any], field: str) -> bool:
@@ -0,0 +1 @@
1
+ __version__ = "0.9.1"
@@ -0,0 +1,28 @@
1
+ """Rule messages must match every other SDK (sdk/contract_rule_messages.json)."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from runapi.core import Resource
9
+ from runapi.core.errors import ValidationError
10
+
11
+ SDK_ROOT = Path(__file__).resolve().parents[4]
12
+ FIXTURE = SDK_ROOT / "contract_rule_messages.json"
13
+ CONTRACT = SDK_ROOT / "contract.json"
14
+
15
+ # The shared fixture lives in the SDK monorepo; the public package repo does not ship it.
16
+ pytestmark = pytest.mark.skipif(not FIXTURE.exists(), reason="shared SDK fixture not available")
17
+
18
+ CASES = json.loads(FIXTURE.read_text())["cases"] if FIXTURE.exists() else []
19
+
20
+
21
+ @pytest.mark.parametrize("case", CASES, ids=[case["name"] for case in CASES])
22
+ def test_rule_message_matches_shared_fixture(case):
23
+ schema = json.loads(CONTRACT.read_text())["actions"][case["action"]]
24
+
25
+ with pytest.raises(ValidationError) as error:
26
+ Resource(object())._validate_contract(schema, dict(case["params"]))
27
+
28
+ assert str(error.value) == case["message"]
@@ -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"})
@@ -155,7 +151,7 @@ def test_validate_boolean_enum_requires_boolean_value():
155
151
 
156
152
  resource._validate_contract(schema, {"model": "m", "flag": True})
157
153
  resource._validate_contract(schema, {"model": "m", "flag": False})
158
- with pytest.raises(ValidationError, match="^flag must be one of: True, False$"):
154
+ with pytest.raises(ValidationError, match="^flag must be one of: true, false$"):
159
155
  resource._validate_contract(schema, {"model": "m", "flag": "true"})
160
156
 
161
157
 
@@ -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.3"