|
10 | 10 |
|
11 | 11 | class FireworksAPIClient: |
12 | 12 | """Client for making authenticated requests to Fireworks API with proper headers. |
13 | | - |
| 13 | +
|
14 | 14 | This client automatically includes: |
15 | 15 | - Authorization header (Bearer token) |
16 | 16 | - User-Agent header for tracking eval-protocol CLI usage |
17 | 17 | """ |
18 | | - |
| 18 | + |
19 | 19 | def __init__(self, api_key: Optional[str] = None, api_base: Optional[str] = None): |
20 | 20 | """Initialize the Fireworks API client. |
21 | | - |
| 21 | +
|
22 | 22 | Args: |
23 | 23 | api_key: Fireworks API key. If None, will be read from environment. |
24 | 24 | api_base: API base URL. If None, defaults to https://api.fireworks.ai |
25 | 25 | """ |
26 | 26 | self.api_key = api_key |
27 | 27 | self.api_base = api_base or os.environ.get("FIREWORKS_API_BASE", "https://api.fireworks.ai") |
28 | 28 | self._session = requests.Session() |
29 | | - |
30 | | - def _get_headers(self, content_type: Optional[str] = "application/json", |
31 | | - additional_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]: |
| 29 | + |
| 30 | + def _validate_path_is_relative(self, path: str) -> None: |
| 31 | + """Validate that the path is relative, not an absolute URL. |
| 32 | +
|
| 33 | + Args: |
| 34 | + path: The path to validate |
| 35 | +
|
| 36 | + Raises: |
| 37 | + ValueError: If path appears to be an absolute URL (starts with http:// or https://) |
| 38 | + """ |
| 39 | + if path.startswith(("http://", "https://")): |
| 40 | + raise ValueError( |
| 41 | + f"Absolute URL detected: '{path}'. FireworksAPIClient methods expect relative paths only. " |
| 42 | + f"Use a relative path like 'v1/path' instead of '{path}'. " |
| 43 | + f"The client will automatically prepend the api_base: '{self.api_base}'" |
| 44 | + ) |
| 45 | + |
| 46 | + def _get_headers( |
| 47 | + self, content_type: Optional[str] = "application/json", additional_headers: Optional[Dict[str, str]] = None |
| 48 | + ) -> Dict[str, str]: |
32 | 49 | """Build headers for API requests. |
33 | | - |
| 50 | +
|
34 | 51 | Args: |
35 | 52 | content_type: Content-Type header value. If None, Content-Type won't be set. |
36 | 53 | additional_headers: Additional headers to merge in. |
37 | | - |
| 54 | +
|
38 | 55 | Returns: |
39 | 56 | Dictionary of headers including authorization and user-agent. |
40 | 57 | """ |
41 | 58 | headers = { |
42 | 59 | "User-Agent": get_user_agent(), |
43 | 60 | } |
44 | | - |
| 61 | + |
45 | 62 | if self.api_key: |
46 | 63 | headers["Authorization"] = f"Bearer {self.api_key}" |
47 | | - |
| 64 | + |
48 | 65 | if content_type: |
49 | 66 | headers["Content-Type"] = content_type |
50 | | - |
| 67 | + |
51 | 68 | if additional_headers: |
52 | 69 | headers.update(additional_headers) |
53 | | - |
| 70 | + |
54 | 71 | return headers |
55 | | - |
56 | | - def get(self, path: str, params: Optional[Dict[str, Any]] = None, |
57 | | - timeout: int = 30, **kwargs) -> requests.Response: |
| 72 | + |
| 73 | + def get( |
| 74 | + self, path: str, params: Optional[Dict[str, Any]] = None, timeout: int = 30, **kwargs |
| 75 | + ) -> requests.Response: |
58 | 76 | """Make a GET request to the Fireworks API. |
59 | | - |
| 77 | +
|
60 | 78 | Args: |
61 | 79 | path: API path (relative to api_base) |
62 | 80 | params: Query parameters |
63 | 81 | timeout: Request timeout in seconds |
64 | 82 | **kwargs: Additional arguments passed to requests.get |
65 | | - |
| 83 | +
|
66 | 84 | Returns: |
67 | 85 | Response object |
68 | 86 | """ |
| 87 | + self._validate_path_is_relative(path) |
69 | 88 | url = f"{self.api_base.rstrip('/')}/{path.lstrip('/')}" |
70 | 89 | headers = self._get_headers(content_type=None) |
71 | 90 | if "headers" in kwargs: |
72 | 91 | headers.update(kwargs.pop("headers")) |
73 | 92 | return self._session.get(url, params=params, headers=headers, timeout=timeout, **kwargs) |
74 | | - |
75 | | - def post(self, path: str, json: Optional[Dict[str, Any]] = None, |
76 | | - data: Optional[Any] = None, files: Optional[Dict[str, Any]] = None, |
77 | | - timeout: int = 60, **kwargs) -> requests.Response: |
| 93 | + |
| 94 | + def post( |
| 95 | + self, |
| 96 | + path: str, |
| 97 | + json: Optional[Dict[str, Any]] = None, |
| 98 | + data: Optional[Any] = None, |
| 99 | + files: Optional[Dict[str, Any]] = None, |
| 100 | + timeout: int = 60, |
| 101 | + **kwargs, |
| 102 | + ) -> requests.Response: |
78 | 103 | """Make a POST request to the Fireworks API. |
79 | | - |
| 104 | +
|
80 | 105 | Args: |
81 | 106 | path: API path (relative to api_base) |
82 | 107 | json: JSON payload |
83 | 108 | data: Form data payload |
84 | 109 | files: Files to upload |
85 | 110 | timeout: Request timeout in seconds |
86 | 111 | **kwargs: Additional arguments passed to requests.post |
87 | | - |
| 112 | +
|
88 | 113 | Returns: |
89 | 114 | Response object |
90 | 115 | """ |
| 116 | + self._validate_path_is_relative(path) |
91 | 117 | url = f"{self.api_base.rstrip('/')}/{path.lstrip('/')}" |
92 | | - |
| 118 | + |
93 | 119 | # For file uploads, don't set Content-Type (let requests handle multipart/form-data) |
94 | 120 | content_type = None if files else "application/json" |
95 | 121 | headers = self._get_headers(content_type=content_type) |
96 | | - |
| 122 | + |
97 | 123 | if "headers" in kwargs: |
98 | 124 | headers.update(kwargs.pop("headers")) |
99 | | - |
100 | | - return self._session.post(url, json=json, data=data, files=files, |
101 | | - headers=headers, timeout=timeout, **kwargs) |
102 | | - |
103 | | - def put(self, path: str, json: Optional[Dict[str, Any]] = None, |
104 | | - timeout: int = 60, **kwargs) -> requests.Response: |
| 125 | + |
| 126 | + return self._session.post(url, json=json, data=data, files=files, headers=headers, timeout=timeout, **kwargs) |
| 127 | + |
| 128 | + def put(self, path: str, json: Optional[Dict[str, Any]] = None, timeout: int = 60, **kwargs) -> requests.Response: |
105 | 129 | """Make a PUT request to the Fireworks API.""" |
| 130 | + self._validate_path_is_relative(path) |
106 | 131 | url = f"{self.api_base.rstrip('/')}/{path.lstrip('/')}" |
107 | 132 | headers = self._get_headers() |
108 | 133 | if "headers" in kwargs: |
109 | 134 | headers.update(kwargs.pop("headers")) |
110 | 135 | return self._session.put(url, json=json, headers=headers, timeout=timeout, **kwargs) |
111 | | - |
112 | | - def patch(self, path: str, json: Optional[Dict[str, Any]] = None, |
113 | | - timeout: int = 60, **kwargs) -> requests.Response: |
| 136 | + |
| 137 | + def patch( |
| 138 | + self, path: str, json: Optional[Dict[str, Any]] = None, timeout: int = 60, **kwargs |
| 139 | + ) -> requests.Response: |
114 | 140 | """Make a PATCH request to the Fireworks API.""" |
| 141 | + self._validate_path_is_relative(path) |
115 | 142 | url = f"{self.api_base.rstrip('/')}/{path.lstrip('/')}" |
116 | 143 | headers = self._get_headers() |
117 | 144 | if "headers" in kwargs: |
118 | 145 | headers.update(kwargs.pop("headers")) |
119 | 146 | return self._session.patch(url, json=json, headers=headers, timeout=timeout, **kwargs) |
120 | | - |
| 147 | + |
121 | 148 | def delete(self, path: str, timeout: int = 30, **kwargs) -> requests.Response: |
122 | 149 | """Make a DELETE request to the Fireworks API.""" |
| 150 | + self._validate_path_is_relative(path) |
123 | 151 | url = f"{self.api_base.rstrip('/')}/{path.lstrip('/')}" |
124 | 152 | headers = self._get_headers(content_type=None) |
125 | 153 | if "headers" in kwargs: |
|
0 commit comments