Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions development/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,24 +106,15 @@ export FIREWORKS_API_BASE="https://dev.api.fireworks.ai" # If targeting dev API

**B. Configuration File (Lower Priority)**

If environment variables are not set, Eval Protocol will attempt to read credentials from `~/.fireworks/auth.ini`.

Create or ensure the file `~/.fireworks/auth.ini` exists with the following format:
```ini
[fireworks]
api_key = YOUR_FIREWORKS_API_KEY
account_id = YOUR_FIREWORKS_ACCOUNT_ID
```
Replace with your actual development credentials if using this method.
Eval Protocol does not read `~/.fireworks/auth.ini` (or any firectl profiles). Use environment variables instead.

**Credential Sourcing Order:**
Eval Protocol prioritizes credentials as follows:
1. Environment Variables (`FIREWORKS_API_KEY`, `FIREWORKS_ACCOUNT_ID`)
2. `~/.fireworks/auth.ini` configuration file
1. Environment Variables (`FIREWORKS_API_KEY`, optional `FIREWORKS_ACCOUNT_ID`)
Comment thread
xzrderek marked this conversation as resolved.
Outdated
Comment thread
xzrderek marked this conversation as resolved.
Outdated

**Purpose of Credentials:**
* `FIREWORKS_API_KEY`: Authenticates your requests to the Fireworks AI service.
* `FIREWORKS_ACCOUNT_ID`: Identifies your account for operations like managing evaluators. It specifies *where* (under which account) an operation should occur.
* `FIREWORKS_ACCOUNT_ID`: Identifies your account for operations like managing evaluators. Typically this is derived automatically from `FIREWORKS_API_KEY` via the `verifyApiKey` endpoint.
* `FIREWORKS_API_BASE`: Allows targeting different API environments (e.g., development, staging).

**Other Environment Variables:**
Expand Down
2 changes: 1 addition & 1 deletion docs/cli_reference/cli_overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ The CLI recognizes the following environment variables:

- `FIREWORKS_API_KEY`: Your Fireworks API key (required for deployment operations)
- `FIREWORKS_API_BASE`: Base URL for the Fireworks API (defaults to `https://api.fireworks.ai`)
- `FIREWORKS_ACCOUNT_ID`: Your Fireworks account ID (optional, can be configured in auth.ini)
- `FIREWORKS_ACCOUNT_ID`: Your Fireworks account ID (optional; typically derived automatically from `FIREWORKS_API_KEY`)
- `MODEL_AGENT`: Default agent model to use (e.g., "openai/gpt-4o-mini")
- `MODEL_SIM`: Default simulation model to use (e.g., "openai/gpt-3.5-turbo")

Expand Down
242 changes: 13 additions & 229 deletions eval_protocol/auth.py
Original file line number Diff line number Diff line change
@@ -1,222 +1,24 @@
import configparser
import logging
import os
from pathlib import Path
from typing import Dict, Optional # Added Dict
from typing import Optional

import requests

logger = logging.getLogger(__name__)

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


def _get_profile_base_dir() -> Path:
"""
Resolve the Fireworks configuration base directory following firectl behavior:
- Default: ~/.fireworks
- If FIREWORKS_PROFILE is set and non-empty: ~/.fireworks/profiles/<profile>
"""
profile_name = os.environ.get("FIREWORKS_PROFILE", "").strip()
base_dir = Path.home() / ".fireworks"
if profile_name:
base_dir = base_dir / "profiles" / profile_name
return base_dir


def _get_auth_ini_file() -> Path:
"""
Determine the auth.ini file path.
Priority:
1) FIREWORKS_AUTH_FILE env var when set
2) ~/.fireworks[/profiles/<profile>]/auth.ini (profile driven)
"""
auth_file_env = os.environ.get("FIREWORKS_AUTH_FILE")
if auth_file_env:
return Path(auth_file_env)
return _get_profile_base_dir() / "auth.ini"


def _is_profile_active() -> bool:
"""
Returns True if a specific profile or explicit auth file is active.
In this case, profile-based credentials should take precedence over env vars.
"""
if os.environ.get("FIREWORKS_AUTH_FILE"):
return True
prof = os.environ.get("FIREWORKS_PROFILE", "").strip()
return bool(prof)


def _parse_simple_auth_file(file_path: Path) -> Dict[str, str]:
"""
Parses an auth file with simple key=value lines.
Handles comments starting with # or ;.
Strips whitespace and basic quotes from values.
"""
creds = {}
if not file_path.exists():
return creds
try:
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or line.startswith(";"):
continue
if "=" in line:
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
# Remove surrounding quotes if present
if value and (
(value.startswith('"') and value.endswith('"'))
or (value.startswith("'") and value.endswith("'"))
):
value = value[1:-1]

if key in ["api_key", "account_id"] and value:
creds[key] = value
except Exception as e:
logger.warning("Error during simple parsing of %s: %s", str(file_path), e)
return creds


def _get_credential_from_config_file(key_name: str) -> Optional[str]:
"""
Helper to get a specific credential (api_key or account_id) from auth.ini.
Tries simple parsing first, then configparser.
"""
auth_ini_path = _get_auth_ini_file()
if not auth_ini_path.exists():
return None

# 1. Try simple key-value parsing first
simple_creds = _parse_simple_auth_file(auth_ini_path)
if key_name in simple_creds:
logger.debug("Using %s from simple key-value parsing of %s.", key_name, str(auth_ini_path))
return simple_creds[key_name]

# 2. Fallback to configparser if not found via simple parsing or if simple parsing failed
# This path will also generate the "no section headers" warning if applicable,
# but only if simple parsing didn't yield the key.
try:
config = configparser.ConfigParser()
config.read(auth_ini_path)

# Try [fireworks] section
if "fireworks" in config and config.has_option("fireworks", key_name):
value_from_file = config.get("fireworks", key_name)
if value_from_file:
logger.debug("Using %s from [fireworks] section in %s.", key_name, str(auth_ini_path))
return value_from_file

# Try default section (configparser might place items without section header here)
if config.has_option(config.default_section, key_name):
value_from_default = config.get(config.default_section, key_name)
if value_from_default:
logger.debug(
"Using %s from default section [%s] in %s.",
key_name,
config.default_section,
str(auth_ini_path),
)
return value_from_default

except configparser.MissingSectionHeaderError:
# This error implies the file is purely key-value, which simple parsing should have handled.
# If simple parsing failed to get the key, then it's likely not there or malformed.
logger.debug("%s has no section headers, and simple parsing did not find %s.", str(auth_ini_path), key_name)
except configparser.Error as e_config:
logger.warning("Configparser error reading %s for %s: %s", str(auth_ini_path), key_name, e_config)
except Exception as e_general:
logger.warning("Unexpected error reading %s for %s: %s", str(auth_ini_path), key_name, e_general)

return None


def _get_credentials_from_config_file() -> Dict[str, Optional[str]]:
"""
Retrieve both api_key and account_id from auth.ini with a single read/parse.
Tries simple parsing first for both keys, then falls back to configparser for any missing ones.
Returns a dict with up to two keys: 'api_key' and 'account_id'.
"""
results: Dict[str, Optional[str]] = {}
auth_ini_path = _get_auth_ini_file()
if not auth_ini_path.exists():
return results

# 1) Simple key=value parsing
try:
simple_creds = _parse_simple_auth_file(auth_ini_path)
if "api_key" in simple_creds and simple_creds["api_key"]:
results["api_key"] = simple_creds["api_key"]
if "account_id" in simple_creds and simple_creds["account_id"]:
results["account_id"] = simple_creds["account_id"]
if "api_key" in results and "account_id" in results:
return results
except Exception as e:
logger.warning("Error during simple parsing of %s: %s", str(auth_ini_path), e)

# 2) ConfigParser for any missing keys
try:
config = configparser.ConfigParser()
config.read(auth_ini_path)
for key_name in ("api_key", "account_id"):
if key_name in results and results[key_name]:
continue
if "fireworks" in config and config.has_option("fireworks", key_name):
value_from_file = config.get("fireworks", key_name)
if value_from_file:
results[key_name] = value_from_file
continue
if config.has_option(config.default_section, key_name):
value_from_default = config.get(config.default_section, key_name)
if value_from_default:
results[key_name] = value_from_default
except configparser.MissingSectionHeaderError:
# Purely key=value file without section headers; simple parsing should have handled it already.
logger.debug("%s has no section headers; falling back to simple parsing results.", str(auth_ini_path))
except configparser.Error as e_config:
logger.warning("Configparser error reading %s: %s", str(auth_ini_path), e_config)
except Exception as e_general:
logger.warning("Unexpected error reading %s: %s", str(auth_ini_path), e_general)

return results


def get_fireworks_api_key() -> Optional[str]:
"""
Retrieves the Fireworks API key.

The key is sourced in the following order:
1. FIREWORKS_API_KEY environment variable.
2. 'api_key' from the [fireworks] section of ~/.fireworks/auth.ini.

Returns:
The API key if found, otherwise None.
"""
# If a profile is active, prefer profile file first, then env
if _is_profile_active():
api_key_from_file = _get_credential_from_config_file("api_key")
if api_key_from_file:
return api_key_from_file
api_key = os.environ.get("FIREWORKS_API_KEY")
if api_key:
logger.debug("Using FIREWORKS_API_KEY from environment variable (profile active but file missing).")
return api_key
else:
# Default behavior: env overrides file
api_key = os.environ.get("FIREWORKS_API_KEY")
if api_key:
logger.debug("Using FIREWORKS_API_KEY from environment variable.")
return api_key
api_key_from_file = _get_credential_from_config_file("api_key")
if api_key_from_file:
return api_key_from_file

logger.debug("Fireworks API key not found in environment variables or auth.ini.")
api_key = os.environ.get("FIREWORKS_API_KEY")
if api_key and api_key.strip():
logger.debug("Using FIREWORKS_API_KEY from environment variable.")
return api_key.strip()
logger.debug("Fireworks API key not found in environment variables.")
return None


Expand All @@ -226,36 +28,18 @@ def get_fireworks_account_id() -> Optional[str]:

The Account ID is sourced in the following order:
1. FIREWORKS_ACCOUNT_ID environment variable.
2. 'account_id' from the [fireworks] section of ~/.fireworks/auth.ini.
3. If an API key is available (env or auth.ini), resolve via verifyApiKey.
2. If an API key is available (env), resolve via verifyApiKey.

Returns:
The Account ID if found, otherwise None.
"""
# If a profile is active, prefer profile file first, then env
if _is_profile_active():
creds = _get_credentials_from_config_file()
account_id_from_file = creds.get("account_id")
if account_id_from_file:
return account_id_from_file
account_id = os.environ.get("FIREWORKS_ACCOUNT_ID")
if account_id:
logger.debug("Using FIREWORKS_ACCOUNT_ID from environment variable (profile active but file missing).")
return account_id
else:
# Default behavior: env overrides file
account_id = os.environ.get("FIREWORKS_ACCOUNT_ID")
if account_id:
logger.debug("Using FIREWORKS_ACCOUNT_ID from environment variable.")
return account_id
creds = _get_credentials_from_config_file()
account_id_from_file = creds.get("account_id")
if account_id_from_file:
return account_id_from_file
account_id = os.environ.get("FIREWORKS_ACCOUNT_ID")
if account_id and account_id.strip():
logger.debug("Using FIREWORKS_ACCOUNT_ID from environment variable.")
return account_id.strip()

# 3) Fallback: if API key is present, attempt to resolve via verifyApiKey (env or auth.ini)
# Fallback: if API key is present, attempt to resolve via verifyApiKey (env)
try:
# Intentionally use get_fireworks_api_key to centralize precedence (env vs file)
api_key_for_verify = get_fireworks_api_key()
if api_key_for_verify:
resolved = verify_api_key_and_get_account_id(api_key=api_key_for_verify, api_base=get_fireworks_api_base())
Expand All @@ -265,7 +49,7 @@ def get_fireworks_account_id() -> Optional[str]:
except Exception as e:
logger.debug("Failed to resolve FIREWORKS_ACCOUNT_ID via verifyApiKey: %s", e)

logger.debug("Fireworks Account ID not found in environment variables, auth.ini, or via verifyApiKey.")
logger.debug("Fireworks Account ID not found in environment variables or via verifyApiKey.")
return None


Expand Down
34 changes: 0 additions & 34 deletions eval_protocol/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ def build_parser() -> argparse.ArgumentParser:
def _configure_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
"""Configure all arguments and subparsers on the given parser."""
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
parser.add_argument(
"--profile",
help="Fireworks profile to use (reads ~/.fireworks/profiles/<name>/auth.ini and settings.ini)",
)
parser.add_argument(
"--server",
help="Fireworks API server hostname or URL (e.g., dev.api.fireworks.ai or https://dev.api.fireworks.ai)",
Expand Down Expand Up @@ -589,38 +585,8 @@ def _extract_flag_value(argv_list, flag_name):
return tok.split("=", 1)[1]
return None

pre_profile = _extract_flag_value(raw_argv, "--profile")
pre_server = _extract_flag_value(raw_argv, "--server")

# Handle Fireworks profile selection early so downstream modules see the env
profile = pre_profile
if profile:
try:
os.environ["FIREWORKS_PROFILE"] = profile
# Mirror firectl behavior: ~/.fireworks[/profiles/<profile>]
base_dir = Path.home() / ".fireworks"
if profile:
base_dir = base_dir / "profiles" / profile
os.makedirs(str(base_dir), mode=0o700, exist_ok=True)

# Provide helpful env hints for consumers (optional)
os.environ["FIREWORKS_AUTH_FILE"] = str(base_dir / "auth.ini")
os.environ["FIREWORKS_SETTINGS_FILE"] = str(base_dir / "settings.ini")
logger.debug("Using Fireworks profile '%s' at %s", profile, base_dir)
except OSError as e:
logger.warning("Failed to initialize Fireworks profile '%s': %s", profile, e)

# Proactively resolve and export account_id from the active profile to avoid stale .env overrides
try:
from eval_protocol.auth import get_fireworks_account_id as _resolve_account_id

resolved_account = _resolve_account_id()
if resolved_account:
os.environ["FIREWORKS_ACCOUNT_ID"] = resolved_account
logger.debug("Resolved account_id from profile '%s': %s", profile, resolved_account)
except Exception as e: # noqa: B902
logger.debug("Unable to resolve account_id from profile '%s': %s", profile, e)

# Handle Fireworks server selection early
server = pre_server
if server:
Expand Down
4 changes: 3 additions & 1 deletion eval_protocol/cli_commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ def upload_command(args: argparse.Namespace) -> int:
print(f"Warning: Failed to create/update {secret_name} secret on Fireworks.")
else:
if not fw_account_id:
print("Warning: FIREWORKS_ACCOUNT_ID not found; cannot register secrets.")
print(
"Warning: Could not resolve Fireworks account id from FIREWORKS_API_KEY; cannot register secrets."
)
if not secrets_from_file:
print("Warning: No API keys found in environment or .env file; no secrets to register.")
except Exception as e:
Expand Down
Loading
Loading