Skip to content

Commit d0f58fa

Browse files
committed
support fireworks dev tier
1 parent 289abc5 commit d0f58fa

4 files changed

Lines changed: 199 additions & 48 deletions

File tree

eval_protocol/auth.py

Lines changed: 94 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,48 @@
66

77
logger = logging.getLogger(__name__)
88

9+
# Default locations (used for tests and as fallback). Actual resolution is dynamic via _get_auth_ini_file().
910
FIREWORKS_CONFIG_DIR = Path.home() / ".fireworks"
1011
AUTH_INI_FILE = FIREWORKS_CONFIG_DIR / "auth.ini"
1112

1213

14+
def _get_profile_base_dir() -> Path:
15+
"""
16+
Resolve the Fireworks configuration base directory following firectl behavior:
17+
- Default: ~/.fireworks
18+
- If FIREWORKS_PROFILE is set and non-empty: ~/.fireworks/profiles/<profile>
19+
"""
20+
profile_name = os.environ.get("FIREWORKS_PROFILE", "").strip()
21+
base_dir = Path.home() / ".fireworks"
22+
if profile_name:
23+
base_dir = base_dir / "profiles" / profile_name
24+
return base_dir
25+
26+
27+
def _get_auth_ini_file() -> Path:
28+
"""
29+
Determine the auth.ini file path.
30+
Priority:
31+
1) FIREWORKS_AUTH_FILE env var when set
32+
2) ~/.fireworks[/profiles/<profile>]/auth.ini (profile driven)
33+
"""
34+
auth_file_env = os.environ.get("FIREWORKS_AUTH_FILE")
35+
if auth_file_env:
36+
return Path(auth_file_env)
37+
return _get_profile_base_dir() / "auth.ini"
38+
39+
40+
def _is_profile_active() -> bool:
41+
"""
42+
Returns True if a specific profile or explicit auth file is active.
43+
In this case, profile-based credentials should take precedence over env vars.
44+
"""
45+
if os.environ.get("FIREWORKS_AUTH_FILE"):
46+
return True
47+
prof = os.environ.get("FIREWORKS_PROFILE", "").strip()
48+
return bool(prof)
49+
50+
1351
def _parse_simple_auth_file(file_path: Path) -> Dict[str, str]:
1452
"""
1553
Parses an auth file with simple key=value lines.
@@ -20,7 +58,7 @@ def _parse_simple_auth_file(file_path: Path) -> Dict[str, str]:
2058
if not file_path.exists():
2159
return creds
2260
try:
23-
with open(file_path, "r") as f:
61+
with open(file_path, "r", encoding="utf-8") as f:
2462
for line in f:
2563
line = line.strip()
2664
if not line or line.startswith("#") or line.startswith(";"):
@@ -39,7 +77,7 @@ def _parse_simple_auth_file(file_path: Path) -> Dict[str, str]:
3977
if key in ["api_key", "account_id"] and value:
4078
creds[key] = value
4179
except Exception as e:
42-
logger.warning(f"Error during simple parsing of {file_path}: {e}")
80+
logger.warning("Error during simple parsing of %s: %s", str(file_path), e)
4381
return creds
4482

4583

@@ -48,44 +86,50 @@ def _get_credential_from_config_file(key_name: str) -> Optional[str]:
4886
Helper to get a specific credential (api_key or account_id) from auth.ini.
4987
Tries simple parsing first, then configparser.
5088
"""
51-
if not AUTH_INI_FILE.exists():
89+
auth_ini_path = _get_auth_ini_file()
90+
if not auth_ini_path.exists():
5291
return None
5392

5493
# 1. Try simple key-value parsing first
55-
simple_creds = _parse_simple_auth_file(AUTH_INI_FILE)
94+
simple_creds = _parse_simple_auth_file(auth_ini_path)
5695
if key_name in simple_creds:
57-
logger.debug(f"Using {key_name} from simple key-value parsing of {AUTH_INI_FILE}.")
96+
logger.debug("Using %s from simple key-value parsing of %s.", key_name, str(auth_ini_path))
5897
return simple_creds[key_name]
5998

6099
# 2. Fallback to configparser if not found via simple parsing or if simple parsing failed
61100
# This path will also generate the "no section headers" warning if applicable,
62101
# but only if simple parsing didn't yield the key.
63102
try:
64103
config = configparser.ConfigParser()
65-
config.read(AUTH_INI_FILE)
104+
config.read(auth_ini_path)
66105

67106
# Try [fireworks] section
68107
if "fireworks" in config and config.has_option("fireworks", key_name):
69108
value_from_file = config.get("fireworks", key_name)
70109
if value_from_file:
71-
logger.debug(f"Using {key_name} from [fireworks] section in {AUTH_INI_FILE}.")
110+
logger.debug("Using %s from [fireworks] section in %s.", key_name, str(auth_ini_path))
72111
return value_from_file
73112

74113
# Try default section (configparser might place items without section header here)
75114
if config.has_option(config.default_section, key_name):
76115
value_from_default = config.get(config.default_section, key_name)
77116
if value_from_default:
78-
logger.debug(f"Using {key_name} from default section [{config.default_section}] in {AUTH_INI_FILE}.")
117+
logger.debug(
118+
"Using %s from default section [%s] in %s.",
119+
key_name,
120+
config.default_section,
121+
str(auth_ini_path),
122+
)
79123
return value_from_default
80124

81125
except configparser.MissingSectionHeaderError:
82126
# This error implies the file is purely key-value, which simple parsing should have handled.
83127
# If simple parsing failed to get the key, then it's likely not there or malformed.
84-
logger.debug(f"{AUTH_INI_FILE} has no section headers, and simple parsing did not find {key_name}.")
128+
logger.debug("%s has no section headers, and simple parsing did not find %s.", str(auth_ini_path), key_name)
85129
except configparser.Error as e_config:
86-
logger.warning(f"Configparser error reading {AUTH_INI_FILE} for {key_name}: {e_config}")
130+
logger.warning("Configparser error reading %s for %s: %s", str(auth_ini_path), key_name, e_config)
87131
except Exception as e_general:
88-
logger.warning(f"Unexpected error reading {AUTH_INI_FILE} for {key_name}: {e_general}")
132+
logger.warning("Unexpected error reading %s for %s: %s", str(auth_ini_path), key_name, e_general)
89133

90134
return None
91135

@@ -101,14 +145,24 @@ def get_fireworks_api_key() -> Optional[str]:
101145
Returns:
102146
The API key if found, otherwise None.
103147
"""
104-
api_key = os.environ.get("FIREWORKS_API_KEY")
105-
if api_key:
106-
logger.debug("Using FIREWORKS_API_KEY from environment variable.")
107-
return api_key
108-
109-
api_key_from_file = _get_credential_from_config_file("api_key")
110-
if api_key_from_file:
111-
return api_key_from_file
148+
# If a profile is active, prefer profile file first, then env
149+
if _is_profile_active():
150+
api_key_from_file = _get_credential_from_config_file("api_key")
151+
if api_key_from_file:
152+
return api_key_from_file
153+
api_key = os.environ.get("FIREWORKS_API_KEY")
154+
if api_key:
155+
logger.debug("Using FIREWORKS_API_KEY from environment variable (profile active but file missing).")
156+
return api_key
157+
else:
158+
# Default behavior: env overrides file
159+
api_key = os.environ.get("FIREWORKS_API_KEY")
160+
if api_key:
161+
logger.debug("Using FIREWORKS_API_KEY from environment variable.")
162+
return api_key
163+
api_key_from_file = _get_credential_from_config_file("api_key")
164+
if api_key_from_file:
165+
return api_key_from_file
112166

113167
logger.debug("Fireworks API key not found in environment variables or auth.ini.")
114168
return None
@@ -125,14 +179,26 @@ def get_fireworks_account_id() -> Optional[str]:
125179
Returns:
126180
The Account ID if found, otherwise None.
127181
"""
128-
account_id = os.environ.get("FIREWORKS_ACCOUNT_ID")
129-
if account_id:
130-
logger.debug("Using FIREWORKS_ACCOUNT_ID from environment variable.")
131-
return account_id
132-
133-
account_id_from_file = _get_credential_from_config_file("account_id")
134-
if account_id_from_file:
135-
return account_id_from_file
182+
# If a profile is active, prefer profile file first, then env
183+
if _is_profile_active():
184+
account_id_from_file = _get_credential_from_config_file("account_id")
185+
if account_id_from_file:
186+
return account_id_from_file
187+
account_id = os.environ.get("FIREWORKS_ACCOUNT_ID")
188+
if account_id:
189+
logger.debug(
190+
"Using FIREWORKS_ACCOUNT_ID from environment variable (profile active but file missing)."
191+
)
192+
return account_id
193+
else:
194+
# Default behavior: env overrides file
195+
account_id = os.environ.get("FIREWORKS_ACCOUNT_ID")
196+
if account_id:
197+
logger.debug("Using FIREWORKS_ACCOUNT_ID from environment variable.")
198+
return account_id
199+
account_id_from_file = _get_credential_from_config_file("account_id")
200+
if account_id_from_file:
201+
return account_id_from_file
136202

137203
logger.debug("Fireworks Account ID not found in environment variables or auth.ini.")
138204
return None
@@ -152,5 +218,5 @@ def get_fireworks_api_base() -> str:
152218
if os.environ.get("FIREWORKS_API_BASE"):
153219
logger.debug("Using FIREWORKS_API_BASE from environment variable.")
154220
else:
155-
logger.debug(f"FIREWORKS_API_BASE not set in environment, defaulting to {api_base}.")
221+
logger.debug("FIREWORKS_API_BASE not set in environment, defaulting to %s.", api_base)
156222
return api_base

eval_protocol/cli.py

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,30 @@
33
"""
44

55
import argparse
6-
import asyncio
7-
import json
86
import logging
97
import os
108
import sys
11-
import traceback
12-
import uuid
139
from pathlib import Path
10+
from typing import Any, cast
1411

1512
logger = logging.getLogger(__name__)
1613

1714

18-
from .cli_commands.agent_eval_cmd import agent_eval_command
1915
from .cli_commands.common import setup_logging
20-
from .cli_commands.deploy import deploy_command
21-
from .cli_commands.deploy_mcp import deploy_mcp_command
22-
from .cli_commands.logs import logs_command
23-
from .cli_commands.preview import preview_command
24-
from .cli_commands.run_eval_cmd import hydra_cli_entry_point
25-
from .cli_commands.upload import upload_command
2616

2717

2818
def parse_args(args=None):
2919
"""Parse command line arguments"""
3020
parser = argparse.ArgumentParser(description="eval-protocol: Tools for evaluation and reward modeling")
3121
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
22+
parser.add_argument(
23+
"--profile",
24+
help="Fireworks profile to use (reads ~/.fireworks/profiles/<name>/auth.ini and settings.ini)",
25+
)
26+
parser.add_argument(
27+
"--server",
28+
help="Fireworks API server hostname or URL (e.g., dev.api.fireworks.ai or https://dev.api.fireworks.ai)",
29+
)
3230

3331
subparsers = parser.add_subparsers(dest="command", help="Command to run")
3432

@@ -356,12 +354,68 @@ def main():
356354
os.environ["PYTHONPATH"] = f"{current_dir}{os.pathsep}{current_pythonpath}"
357355
else:
358356
os.environ["PYTHONPATH"] = current_dir
359-
logger.debug(f"Added current directory to PYTHONPATH: {current_dir}")
357+
logger.debug("Added current directory to PYTHONPATH: %s", current_dir)
360358

361359
# Also add to sys.path so it takes effect immediately for the current process
362360
if current_dir not in sys.path:
363361
sys.path.insert(0, current_dir)
364362

363+
# Pre-scan raw argv for global flags anywhere (before parsing or imports)
364+
raw_argv = sys.argv[1:]
365+
366+
def _extract_flag_value(argv_list, flag_name):
367+
# Supports --flag value and --flag=value
368+
for i, tok in enumerate(argv_list):
369+
if tok == flag_name:
370+
if i + 1 < len(argv_list):
371+
return argv_list[i + 1]
372+
elif tok.startswith(flag_name + "="):
373+
return tok.split("=", 1)[1]
374+
return None
375+
376+
pre_profile = _extract_flag_value(raw_argv, "--profile")
377+
pre_server = _extract_flag_value(raw_argv, "--server")
378+
379+
# Handle Fireworks profile selection early so downstream modules see the env
380+
profile = pre_profile
381+
if profile:
382+
try:
383+
os.environ["FIREWORKS_PROFILE"] = profile
384+
# Mirror firectl behavior: ~/.fireworks[/profiles/<profile>]
385+
base_dir = Path.home() / ".fireworks"
386+
if profile:
387+
base_dir = base_dir / "profiles" / profile
388+
os.makedirs(str(base_dir), mode=0o700, exist_ok=True)
389+
390+
# Provide helpful env hints for consumers (optional)
391+
os.environ["FIREWORKS_AUTH_FILE"] = str(base_dir / "auth.ini")
392+
os.environ["FIREWORKS_SETTINGS_FILE"] = str(base_dir / "settings.ini")
393+
logger.debug("Using Fireworks profile '%s' at %s", profile, base_dir)
394+
except OSError as e:
395+
logger.warning("Failed to initialize Fireworks profile '%s': %s", profile, e)
396+
397+
# Proactively resolve and export account_id from the active profile to avoid stale .env overrides
398+
try:
399+
from eval_protocol.auth import get_fireworks_account_id as _resolve_account_id
400+
401+
resolved_account = _resolve_account_id()
402+
if resolved_account:
403+
os.environ["FIREWORKS_ACCOUNT_ID"] = resolved_account
404+
logger.debug("Resolved account_id from profile '%s': %s", profile, resolved_account)
405+
except Exception as e: # noqa: B902
406+
logger.debug("Unable to resolve account_id from profile '%s': %s", profile, e)
407+
408+
# Handle Fireworks server selection early
409+
server = pre_server
410+
if server:
411+
# Normalize to full URL if just a hostname is supplied
412+
normalized = server.strip()
413+
if not normalized.startswith("http://") and not normalized.startswith("https://"):
414+
normalized = f"https://{normalized}"
415+
os.environ["FIREWORKS_API_BASE"] = normalized
416+
logger.debug("Using Fireworks API base: %s", normalized)
417+
418+
# Now parse args normally (so help/commands work), after globals applied
365419
# Store original sys.argv[0] because Hydra might manipulate it
366420
# and we need it if we're not calling a Hydra app.
367421
original_script_name = sys.argv[0]
@@ -370,16 +424,22 @@ def main():
370424
setup_logging(args.verbose, getattr(args, "debug", False))
371425

372426
if args.command == "preview":
427+
from .cli_commands.preview import preview_command
373428
return preview_command(args)
374429
elif args.command == "deploy":
430+
from .cli_commands.deploy import deploy_command
375431
return deploy_command(args)
376432
elif args.command == "deploy-mcp":
433+
from .cli_commands.deploy_mcp import deploy_mcp_command
377434
return deploy_mcp_command(args)
378435
elif args.command == "agent-eval":
436+
from .cli_commands.agent_eval_cmd import agent_eval_command
379437
return agent_eval_command(args)
380438
elif args.command == "logs":
439+
from .cli_commands.logs import logs_command
381440
return logs_command(args)
382441
elif args.command == "upload":
442+
from .cli_commands.upload import upload_command
383443
return upload_command(args)
384444
elif args.command == "run":
385445
# For the 'run' command, Hydra takes over argument parsing.
@@ -393,7 +453,7 @@ def main():
393453
local_conf_dir = os.path.join(current_dir, "conf")
394454

395455
if not has_config_path and os.path.isdir(local_conf_dir):
396-
logger.info(f"Auto-detected local conf directory: {local_conf_dir}")
456+
logger.info("Auto-detected local conf directory: %s", local_conf_dir)
397457
hydra_specific_args = [
398458
"--config-path",
399459
local_conf_dir,
@@ -410,33 +470,38 @@ def main():
410470
path_val = hydra_specific_args[i]
411471
abs_path = os.path.abspath(path_val)
412472
logger.debug(
413-
f"Converting relative --config-path '{path_val}' (space separated) to absolute '{abs_path}'"
473+
"Converting relative --config-path '%s' (space separated) to absolute '%s'",
474+
path_val,
475+
abs_path,
414476
)
415477
processed_hydra_args.append(abs_path)
416478
else:
417479
logger.error("--config-path specified without a value.")
418-
pass
419480
elif arg.startswith("--config-path="):
420481
flag_part, path_val = arg.split("=", 1)
421482
processed_hydra_args.append(flag_part)
422483
abs_path = os.path.abspath(path_val)
423484
logger.debug(
424-
f"Converting relative --config-path '{path_val}' (equals separated) to absolute '{abs_path}'"
485+
"Converting relative --config-path '%s' (equals separated) to absolute '%s'",
486+
path_val,
487+
abs_path,
425488
)
426489
processed_hydra_args.append(abs_path)
427490
else:
428491
processed_hydra_args.append(arg)
429492
i += 1
430493

431494
sys.argv = [sys.argv[0]] + processed_hydra_args
432-
logger.info(f"SYSCALL_ARGV_FOR_HYDRA (after potential abspath conversion): {sys.argv}")
495+
logger.info("SYSCALL_ARGV_FOR_HYDRA (after potential abspath conversion): %s", sys.argv)
433496

434497
try:
435-
hydra_cli_entry_point()
498+
from .cli_commands.run_eval_cmd import hydra_cli_entry_point
499+
hydra_entry = cast(Any, hydra_cli_entry_point)
500+
hydra_entry() # type: ignore # pylint: disable=no-value-for-parameter
436501
return 0
437-
except Exception as e:
502+
except Exception as e: # pylint: disable=broad-except
438503
error_msg = str(e)
439-
logger.error(f"Evaluation failed: {e}")
504+
logger.error("Evaluation failed: %s", e)
440505

441506
# Provide helpful suggestions for common Hydra/config errors
442507
if "Cannot find primary config" in error_msg:

0 commit comments

Comments
 (0)