Skip to content

Commit 1ae8143

Browse files
authored
check FIREWORKS_API_KEY in upload flow (#255)
* check FIREWORKS_API_KEY in upload flow * normalize resource id
1 parent 289abc5 commit 1ae8143

File tree

2 files changed

+78
-26
lines changed

2 files changed

+78
-26
lines changed

eval_protocol/cli_commands/upload.py

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from typing import Any, Callable, Iterable, Optional
1313

1414
import pytest
15+
from eval_protocol.auth import get_fireworks_account_id, get_fireworks_api_key
16+
from eval_protocol.platform_api import create_or_update_fireworks_secret
1517

1618
from eval_protocol.evaluation import create_evaluation
1719

@@ -343,28 +345,45 @@ def _prompt_select_interactive(tests: list[DiscoveredTest]) -> list[DiscoveredTe
343345
else:
344346
return []
345347

346-
# Create choices with nice formatting
347-
choices = []
348-
for idx, test in enumerate(tests, 1):
349-
choice_text = _format_test_choice(test, idx)
350-
choices.append({"name": choice_text, "value": idx - 1, "checked": False})
348+
# Enter-only selection UX with optional multi-select via repeat
349+
remaining_indices = list(range(len(tests)))
350+
selected_indices: list[int] = []
351351

352352
print("\n")
353-
print("💡 Tip: Use ↑/↓ arrows to navigate, SPACE to select/deselect, ENTER when done")
354-
print(" You can select multiple tests!\n")
355-
selected_indices = questionary.checkbox(
356-
"Select evaluation tests to upload:",
357-
choices=choices,
358-
style=custom_style,
359-
).ask()
360-
361-
if selected_indices is None: # User pressed Ctrl+C
362-
print("\nUpload cancelled.")
363-
return []
353+
print("Tip: Use ↑/↓ arrows to navigate and press ENTER to select.")
354+
print(" After selecting one, you can choose to add more.\n")
355+
356+
while remaining_indices:
357+
# Build choices from remaining
358+
choices = []
359+
for idx, test_idx in enumerate(remaining_indices, 1):
360+
t = tests[test_idx]
361+
choice_text = _format_test_choice(t, idx)
362+
choices.append({"name": choice_text, "value": test_idx})
363+
364+
selected = questionary.select(
365+
"Select an evaluation test to upload:", choices=choices, style=custom_style
366+
).ask()
367+
368+
if selected is None: # Ctrl+C
369+
print("\nUpload cancelled.")
370+
return []
371+
372+
if isinstance(selected, int):
373+
selected_indices.append(selected)
374+
# Remove from remaining
375+
if selected in remaining_indices:
376+
remaining_indices.remove(selected)
377+
378+
# Ask whether to add another (ENTER to finish)
379+
add_more = questionary.confirm("Add another?", default=False, style=custom_style).ask()
380+
if not add_more:
381+
break
382+
else:
383+
break
364384

365385
if not selected_indices:
366386
print("\n⚠️ No tests were selected.")
367-
print(" Remember: Use SPACE bar to select tests, then press ENTER to confirm.")
368387
return []
369388

370389
print(f"\n✓ Selected {len(selected_indices)} test(s)")
@@ -474,6 +493,28 @@ def upload_command(args: argparse.Namespace) -> int:
474493
description = getattr(args, "description", None)
475494
force = bool(getattr(args, "force", False))
476495

496+
# Ensure FIREWORKS_API_KEY is available to the remote by storing it as a Fireworks secret
497+
try:
498+
fw_account_id = get_fireworks_account_id()
499+
fw_api_key_value = get_fireworks_api_key()
500+
if fw_account_id and fw_api_key_value:
501+
print("Ensuring FIREWORKS_API_KEY is registered as a secret on Fireworks for rollout...")
502+
if create_or_update_fireworks_secret(
503+
account_id=fw_account_id,
504+
key_name="FIREWORKS_API_KEY",
505+
secret_value=fw_api_key_value,
506+
):
507+
print("✓ FIREWORKS_API_KEY secret created/updated on Fireworks.")
508+
else:
509+
print("Warning: Failed to create/update FIREWORKS_API_KEY secret on Fireworks.")
510+
else:
511+
if not fw_account_id:
512+
print("Warning: FIREWORKS_ACCOUNT_ID not found; cannot register FIREWORKS_API_KEY secret.")
513+
if not fw_api_key_value:
514+
print("Warning: FIREWORKS_API_KEY not found locally; cannot register secret.")
515+
except Exception as e:
516+
print(f"Warning: Skipped Fireworks secret registration due to error: {e}")
517+
477518
exit_code = 0
478519
for i, (code, file_name, qualname, source_file_path) in enumerate(selected_specs):
479520
# Use ts_mode to upload evaluator

eval_protocol/platform_api.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ def __str__(self) -> str:
5353
return f"{super().__str__()} (Status: {self.status_code}, Response: {self.response_text or 'N/A'})"
5454

5555

56+
def _normalize_secret_resource_id(key_name: str) -> str:
57+
"""
58+
Normalize a secret's resource ID for Fireworks paths:
59+
- Lowercase
60+
- Replace underscores with hyphens
61+
- Leave other characters as-is (server enforces allowed set)
62+
"""
63+
return key_name.lower().replace("_", "-")
64+
65+
5666
def create_or_update_fireworks_secret(
5767
account_id: str,
5868
key_name: str, # This is the identifier for the secret, e.g., "my-eval-api-key"
@@ -95,8 +105,9 @@ def create_or_update_fireworks_secret(
95105
# Let's assume for POST, we send 'keyName' and 'value'.
96106
# For PATCH, the path contains {secret_id} which is the key_name. The body is also gatewaySecret.
97107

98-
# Check if secret exists using GET (path uses secret_id which is our key_name)
99-
get_url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets/{key_name}"
108+
# Check if secret exists using GET (path uses normalized resource id)
109+
resource_id = _normalize_secret_resource_id(key_name)
110+
get_url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets/{resource_id}"
100111
secret_exists = False
101112
try:
102113
response = requests.get(get_url, headers=headers, timeout=10)
@@ -120,7 +131,7 @@ def create_or_update_fireworks_secret(
120131

121132
if secret_exists:
122133
# Update existing secret (PATCH)
123-
patch_url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets/{key_name}"
134+
patch_url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets/{resource_id}"
124135
# Body for PATCH requires 'keyName' and 'value'.
125136
# Transform key_name for payload: uppercase and underscores
126137
payload_key_name = key_name.upper().replace("-", "_")
@@ -148,16 +159,14 @@ def create_or_update_fireworks_secret(
148159
else:
149160
# Create new secret (POST)
150161
post_url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets"
151-
# Body for POST is gatewaySecret. 'name' field in payload is tricky.
162+
# Body for POST is gatewaySecret. 'name' field in payload is the resource path.
152163
# Let's assume for POST, the 'name' in payload can be omitted or is the key_name.
153164
# The API should ideally use 'keyName' from URL or a specific 'secretId' in payload for creation if 'name' is server-assigned.
154165
# Given the Swagger, 'name' is required in gatewaySecret.
155166
# Let's try with 'name' being the 'key_name' for the payload, as the full path is not known yet.
156167
# This might need adjustment based on actual API behavior.
157168
# Construct the full 'name' path for the POST payload as per Swagger's title for 'name'
158-
full_resource_name_for_payload = (
159-
f"accounts/{resolved_account_id}/secrets/{key_name}" # Path uses lowercase-hyphenated key_name
160-
)
169+
full_resource_name_for_payload = f"accounts/{resolved_account_id}/secrets/{resource_id}"
161170

162171
# Transform key_name for payload "keyName" field: uppercase and underscores
163172
payload_key_name = key_name.upper().replace("-", "_")
@@ -209,7 +218,8 @@ def get_fireworks_secret(
209218
return None
210219

211220
headers = {"Authorization": f"Bearer {resolved_api_key}"}
212-
url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets/{key_name}"
221+
resource_id = _normalize_secret_resource_id(key_name)
222+
url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets/{resource_id}"
213223

214224
try:
215225
response = requests.get(url, headers=headers, timeout=10)
@@ -245,7 +255,8 @@ def delete_fireworks_secret(
245255
return False
246256

247257
headers = {"Authorization": f"Bearer {resolved_api_key}"}
248-
url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets/{key_name}"
258+
resource_id = _normalize_secret_resource_id(key_name)
259+
url = f"{resolved_api_base.rstrip('/')}/v1/accounts/{resolved_account_id}/secrets/{resource_id}"
249260

250261
try:
251262
response = requests.delete(url, headers=headers, timeout=30)

0 commit comments

Comments
 (0)