|
9 | 9 | from openai.types.responses.response_output_text import ResponseOutputText |
10 | 10 | from pydantic import SecretStr |
11 | 11 |
|
| 12 | +from openhands.sdk import ConversationStats, RegistryEvent |
12 | 13 | from openhands.sdk.llm import LLM, LLMResponse, Message, TextContent |
13 | 14 | from openhands.sdk.llm.exceptions import LLMNoResponseError |
14 | 15 | from openhands.sdk.llm.options.responses_options import select_responses_options |
@@ -1095,6 +1096,84 @@ def test_issue_2459_restore_metrics_syncs_telemetry(): |
1095 | 1096 | assert llm.telemetry.metrics is llm.metrics |
1096 | 1097 |
|
1097 | 1098 |
|
| 1099 | +@pytest.fixture |
| 1100 | +def llm(): |
| 1101 | + """Create a minimal SDK LLM for testing.""" |
| 1102 | + return LLM( |
| 1103 | + model="openai/gpt-4o", |
| 1104 | + api_key=SecretStr("test-key"), |
| 1105 | + usage_id="test-service", |
| 1106 | + ) |
| 1107 | + |
| 1108 | + |
| 1109 | +def test_cost_recorded_in_restored_metrics(llm): |
| 1110 | + """Costs added via telemetry after restore must land in the restored Metrics.""" |
| 1111 | + restored = Metrics(model_name="openai/gpt-4o") |
| 1112 | + restored.add_cost(5.00) |
| 1113 | + llm.restore_metrics(restored) |
| 1114 | + |
| 1115 | + llm.telemetry.metrics.add_cost(0.50) |
| 1116 | + |
| 1117 | + assert llm.metrics.accumulated_cost == 5.50 |
| 1118 | + assert len(llm.metrics.costs) == 2 |
| 1119 | + |
| 1120 | + |
| 1121 | +def test_stale_metrics_not_updated(llm): |
| 1122 | + """The original (pre-restore) Metrics must not receive new costs.""" |
| 1123 | + original_metrics = llm.metrics |
| 1124 | + |
| 1125 | + restored = Metrics(model_name="openai/gpt-4o") |
| 1126 | + restored.add_cost(2.00) |
| 1127 | + llm.restore_metrics(restored) |
| 1128 | + |
| 1129 | + llm.telemetry.metrics.add_cost(0.75) |
| 1130 | + |
| 1131 | + assert original_metrics.accumulated_cost == 0.0 |
| 1132 | + assert llm.metrics.accumulated_cost == 2.75 |
| 1133 | + |
| 1134 | + |
| 1135 | +def test_restore_metrics_telemetry_none(): |
| 1136 | + """restore_metrics() must not crash when telemetry has not been initialized.""" |
| 1137 | + llm = LLM( |
| 1138 | + model="openai/gpt-4o", |
| 1139 | + api_key=SecretStr("test-key"), |
| 1140 | + usage_id="test-service", |
| 1141 | + ) |
| 1142 | + llm._telemetry = None |
| 1143 | + |
| 1144 | + restored = Metrics(model_name="openai/gpt-4o") |
| 1145 | + restored.add_cost(1.00) |
| 1146 | + llm.restore_metrics(restored) |
| 1147 | + |
| 1148 | + assert llm.metrics is restored |
| 1149 | + assert llm.metrics.accumulated_cost == 1.00 |
| 1150 | + |
| 1151 | + |
| 1152 | +def test_conversation_stats_restore_then_track(): |
| 1153 | + """End-to-end: ConversationStats restores metrics, then new costs are tracked.""" |
| 1154 | + saved_metrics = Metrics(model_name="openai/gpt-4o") |
| 1155 | + saved_metrics.add_cost(10.00) |
| 1156 | + |
| 1157 | + stats = ConversationStats(usage_to_metrics={"agent": saved_metrics}) |
| 1158 | + |
| 1159 | + with patch("openhands.sdk.llm.llm.litellm_completion"): |
| 1160 | + llm = LLM( |
| 1161 | + model="openai/gpt-4o", |
| 1162 | + api_key=SecretStr("test-key"), |
| 1163 | + usage_id="agent", |
| 1164 | + ) |
| 1165 | + event = RegistryEvent(llm=llm) |
| 1166 | + stats.register_llm(event) |
| 1167 | + |
| 1168 | + assert llm.metrics.accumulated_cost == 10.00 |
| 1169 | + |
| 1170 | + # Simulate a new LLM response adding cost via telemetry |
| 1171 | + llm.telemetry.metrics.add_cost(0.25) |
| 1172 | + |
| 1173 | + assert llm.metrics.accumulated_cost == 10.25 |
| 1174 | + assert stats.get_combined_metrics().accumulated_cost == 10.25 |
| 1175 | + |
| 1176 | + |
1098 | 1177 | # max_output_tokens Capping Tests |
1099 | 1178 |
|
1100 | 1179 |
|
|
0 commit comments