python-fastllm 0.0.48__tar.gz → 0.0.49__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/PKG-INFO +1 -1
  2. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/__init__.py +1 -1
  3. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/_modidx.py +1 -1
  4. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/acomplete.py +11 -10
  5. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/anthropic.py +6 -2
  6. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/chat.py +3 -1
  7. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/gemini.py +4 -1
  8. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/openai_chat.py +4 -1
  9. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/openai_responses.py +4 -1
  10. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/types.py +6 -5
  11. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/python_fastllm.egg-info/PKG-INFO +1 -1
  12. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/README.md +0 -0
  13. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/codex.py +0 -0
  14. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/responses.py +0 -0
  15. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/fastllm/streaming.py +0 -0
  16. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/pyproject.toml +0 -0
  17. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/python_fastllm.egg-info/SOURCES.txt +0 -0
  18. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/python_fastllm.egg-info/dependency_links.txt +0 -0
  19. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/python_fastllm.egg-info/entry_points.txt +0 -0
  20. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/python_fastllm.egg-info/requires.txt +0 -0
  21. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/python_fastllm.egg-info/top_level.txt +0 -0
  22. {python_fastllm-0.0.48 → python_fastllm-0.0.49}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.48
3
+ Version: 0.0.49
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -2,4 +2,4 @@
2
2
 
3
3
  - `fastllm.responses`: Provider-independent Responses resources and streaming over FastLLM"""
4
4
 
5
- __version__ = "0.0.48"
5
+ __version__ = "0.0.49"
@@ -338,7 +338,7 @@ d = { 'settings': { 'branch': 'main',
338
338
  'fastllm.types.Usage.__add__': ('types.html#usage.__add__', 'fastllm/types.py'),
339
339
  'fastllm.types.Usage.__eq__': ('types.html#usage.__eq__', 'fastllm/types.py'),
340
340
  'fastllm.types.Usage.__init__': ('types.html#usage.__init__', 'fastllm/types.py'),
341
- 'fastllm.types._rm_ctx_tiers': ('types.html#_rm_ctx_tiers', 'fastllm/types.py'),
341
+ 'fastllm.types._flat_rates': ('types.html#_flat_rates', 'fastllm/types.py'),
342
342
  'fastllm.types.approx_pricing': ('types.html#approx_pricing', 'fastllm/types.py'),
343
343
  'fastllm.types.effort_code': ('types.html#effort_code', 'fastllm/types.py'),
344
344
  'fastllm.types.effort_levels': ('types.html#effort_levels', 'fastllm/types.py'),
@@ -67,10 +67,10 @@ def _codex_mtime_policy():
67
67
 
68
68
  # %% ../nbs/06_acomplete.ipynb #79075d95
69
69
  @flexicache(_codex_mtime_policy())
70
- def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_url=None, xtra_hdrs=None,
70
+ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, oauth_token=None, base_url=None, xtra_hdrs=None,
71
71
  timeout=httpx2.Timeout(connect=30, read=300, write=30, pool=10)):
72
72
  err_msg = f"please pass a valid one vendor: {', '.join(list(vendor_mapping))} or pass `api_name`,`base_url` and `api_key`"
73
- if not vendor_name and not api_name and not (base_url and api_key):
73
+ if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
74
74
  v, m = split_vendor(model)
75
75
  if v in vendor_mapping: vendor_name, model = v, m
76
76
  if vendor_name:
@@ -78,17 +78,17 @@ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_ur
78
78
  try:
79
79
  api_name, base_url, env_api_nm, *auth_json = vendor_mapping[vendor_name]
80
80
  base_url = override_base_url or base_url
81
- if auth_json and not api_key and not os.getenv(env_api_nm):
81
+ if auth_json and not api_key and not oauth_token and not os.getenv(env_api_nm):
82
82
  fn,keys = auth_json[0] # pyright: ignore[reportAssignmentType]
83
83
  auth_fn = Path(fn).expanduser()
84
84
  if auth_fn.exists(): api_key = nested_idx(json.loads(auth_fn.read_text()), *keys)
85
- api_key = get_api_key(api_key, env_api_nm)
85
+ if not oauth_token: api_key = get_api_key(api_key, env_api_nm)
86
86
  except KeyError: raise ValueError(f"Unknown vendor '{vendor_name}', {err_msg}")
87
- elif base_url and api_key: vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
87
+ elif base_url and (api_key or oauth_token): vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
88
88
  elif (api_name:=infer_api_name(model)): base_url, vendor_name = vendor_mapping[api_name][1], api_name
89
89
  else: raise ValueError(f"Model {model} can't be auto resolved, {err_msg}")
90
90
  api = api_registry[api_name]
91
- hdrs = merge(api.get_hdrs(api_key), ifnone(xtra_hdrs, {}))
91
+ hdrs = merge(api.get_hdrs(api_key, oauth_token), ifnone(xtra_hdrs, {}))
92
92
  cli = AsyncHttpCli(base_url, base_headers=hdrs, timeout=timeout)
93
93
  return cli, api_name, vendor_name
94
94
 
@@ -155,18 +155,19 @@ async def _retry_stream(mk_gen, retries=2, retry_delay=0.5):
155
155
 
156
156
  # %% ../nbs/06_acomplete.ipynb #2379ec94
157
157
  @delegates(payload_kwargs)
158
- async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None, base_url=None, endpoint=None, xtra_body=None, xtra_hdrs=None,
158
+ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None, oauth_token=None, base_url=None, endpoint=None, xtra_body=None, xtra_hdrs=None,
159
159
  stream=False, previous_response_id=None, stop_callables=None, retries=2, retry_delay=0.5, **kwargs):
160
160
  "Unified completion across different APIs."
161
- if not vendor_name and not api_name and not (base_url and api_key):
161
+ if api_key and oauth_token: raise ValueError('Pass `api_key` or `oauth_token`, not both: a request has one identity')
162
+ if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
162
163
  v, m = split_vendor(model)
163
164
  if v in vendor_mapping: vendor_name, model = v, m
164
165
  elif v: api_name, model = v, m # a registered transport api (e.g. claude_code): not an HTTP vendor
165
166
  own = api_name in api_registry and not hasattr(api_registry[api_name], 'endpoint') if api_name else False
166
167
  if own:
167
168
  vendor_name = ifnone(vendor_name, api_name) # an own-transport api: no HTTP client to build
168
- if api_key: kwargs['api_key'] = api_key # its `mk_payload` applies a per-user credential itself
169
- else: cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, base_url=base_url, xtra_hdrs=xtra_hdrs)
169
+ if oauth_token: kwargs['oauth_token'] = oauth_token # its `mk_payload` applies the user's token itself
170
+ else: cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, oauth_token=oauth_token, base_url=base_url, xtra_hdrs=xtra_hdrs)
170
171
  api = api_registry[api_name]
171
172
  if previous_response_id is not None:
172
173
  if not getattr(api, 'supports_previous_response_id', False):
@@ -322,8 +322,12 @@ def mk_payload(msgs, model, **kwargs):
322
322
  return payload
323
323
 
324
324
  # %% ../nbs/04_anthropic.ipynb #60d52c1f
325
- def get_hdrs(api_key=None):
326
- return {"x-api-key": get_api_key(api_key, 'ANTHROPIC_API_KEY'), "anthropic-version": "2023-06-01"}
325
+ def get_hdrs(api_key=None, oauth_token=None):
326
+ "An OAuth token travels as a bearer under Anthropic's OAuth beta; an API key as `x-api-key`"
327
+ hdrs = {"anthropic-version": "2023-06-01"}
328
+ if oauth_token: return hdrs | {"Authorization": f"Bearer {oauth_token}", "anthropic-beta": "oauth-2025-04-20"}
329
+ return hdrs | {"x-api-key": get_api_key(api_key, 'ANTHROPIC_API_KEY')}
330
+
327
331
 
328
332
  # %% ../nbs/04_anthropic.ipynb #0d03643a
329
333
  def cost(usage, m):
@@ -168,6 +168,7 @@ class AsyncChat:
168
168
  api_name=None, # API to use, one of ApiName: openai (responses), openai_chat, anthropic, gemini
169
169
  vendor_name=None, # Vendor name, one of vendor_mapping which resolves api_base/api_key automatically
170
170
  api_key=None, # API key when model can't be resolved or vendor_name is not known or codex
171
+ oauth_token=None, # A subscription's OAuth token (Codex, Claude Code) in place of an API key
171
172
  base_url=None, # API base url when model can't be resolved or vendor_name is not known
172
173
  endpoint=None, # Override the transport's request path, for a server mounting Responses at a custom location
173
174
  extra_headers=None, # Extra HTTP headers for custom providers
@@ -178,7 +179,7 @@ class AsyncChat:
178
179
  default_cbs=True # Whether to include default callbacks
179
180
  ):
180
181
  "LiteLLM chat client."
181
- if not vendor_name and not api_name and not (base_url and api_key):
182
+ if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
182
183
  v, model = split_vendor(model)
183
184
  if v in vendor_mapping: vendor_name = v
184
185
  elif v: api_name = v # a registered transport api (e.g. claude_code)
@@ -248,6 +249,7 @@ def _prep_call(self:AsyncChat, search, max_tokens, kwargs, stream=False, think=N
248
249
  if self.api_name: kwargs['api_name'] = self.api_name
249
250
  if self.vendor_name: kwargs['vendor_name'] = self.vendor_name
250
251
  if self.api_key: kwargs['api_key'] = self.api_key
252
+ if self.oauth_token: kwargs['oauth_token'] = self.oauth_token
251
253
  if self.base_url: kwargs['base_url'] = self.base_url
252
254
  if self.endpoint: kwargs['endpoint'] = self.endpoint
253
255
  if self.extra_headers: kwargs['xtra_hdrs'] = self.extra_headers
@@ -267,9 +267,12 @@ def mk_payload(msgs, model, **kwargs):
267
267
  return payload
268
268
 
269
269
  # %% ../nbs/05_gemini.ipynb #60d52c1f
270
- def get_hdrs(api_key=None):
270
+ def get_hdrs(api_key=None, oauth_token=None):
271
+ "Bearer auth for an OAuth token; otherwise the API key header"
272
+ if oauth_token: return {"Authorization": f"Bearer {oauth_token}"}
271
273
  return {"x-goog-api-key": get_api_key(api_key, 'GEMINI_API_KEY')}
272
274
 
275
+
273
276
  # %% ../nbs/05_gemini.ipynb #4ee3891f
274
277
  def cost(usage, m):
275
278
  raw = usage.raw
@@ -198,7 +198,10 @@ def mk_payload(msgs, model, **kwargs):
198
198
  return payload
199
199
 
200
200
  # %% ../nbs/03_oai_chat.ipynb #16e813d2
201
- def get_hdrs(api_key=None): return {"Authorization": f"Bearer {get_api_key(api_key, 'OPENAI_API_KEY')}"}
201
+ def get_hdrs(api_key=None, oauth_token=None):
202
+ "Bearer auth: an OAuth access token, or else an API key from the argument or the environment"
203
+ return {"Authorization": f"Bearer {oauth_token or get_api_key(api_key, 'OPENAI_API_KEY')}"}
204
+
202
205
 
203
206
  # %% ../nbs/03_oai_chat.ipynb #f89e2bf6
204
207
  def cost(usage, m):
@@ -256,7 +256,10 @@ def mk_payload(msgs, model, **kwargs):
256
256
  return payload
257
257
 
258
258
  # %% ../nbs/02_oai_responses.ipynb #529fd4bc
259
- def get_hdrs(api_key=None): return {"Authorization": f"Bearer {get_api_key(api_key, 'OPENAI_API_KEY')}"}
259
+ def get_hdrs(api_key=None, oauth_token=None):
260
+ "Bearer auth: an OAuth access token, or else an API key from the argument or the environment"
261
+ return {"Authorization": f"Bearer {oauth_token or get_api_key(api_key, 'OPENAI_API_KEY')}"}
262
+
260
263
 
261
264
  # %% ../nbs/02_oai_responses.ipynb #a907ffa8
262
265
  def cost(usage, m):
@@ -300,14 +300,14 @@ register_model_info('muse-spark-1.1', vendor_name='meta_ai', **modern_llm, max_i
300
300
  codex_pricing = dict(input_cost_per_token = 0.10/1_000_000, output_cost_per_token = 0.50/1_000_000,
301
301
  cache_creation_input_token_cost = 0.10/1_000_000, cache_read_input_token_cost = 0.10/1_000_000)
302
302
 
303
- def _rm_ctx_tiers(vendor_name, model):
304
- "Drop upstream context-tier rates, which would otherwise override flat subscription pricing"
303
+ def _flat_rates(vendor_name, model):
304
+ "Keep only the flat subscription rates: drop the upstream context, cache-TTL and service-tier variants that would otherwise apply"
305
305
  info = model_info_registry[vendor_name, model]
306
- for k in [k for k in info if re.search(r'_above_\d+k_tokens', k)]: del info[k]
306
+ for k in [k for k in info if 'cost' in k and k not in codex_pricing and k != 'search_context_cost_per_query']: del info[k]
307
307
 
308
308
  for model in (codex54, codex54m, codex55):
309
309
  register_model_info(model, 'codex', base=model, base_vendor_name='chatgpt', supports_web_search=True, max_input_tokens=256000, **codex_pricing)
310
- _rm_ctx_tiers('codex', model)
310
+ _flat_rates('codex', model)
311
311
 
312
312
  register_model_info(codex53spark, 'codex', **codex_pricing,
313
313
  supports_vision=False, supports_image_input=False, supports_web_search=True, supports_reasoning=True, supports_function_calling=True,
@@ -318,6 +318,7 @@ register_model_info(codex53spark, 'codex', **codex_pricing,
318
318
  register_model_info(fable51, 'anthropic', base=fable5)
319
319
  for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5, fable51):
320
320
  register_model_info(model, 'claude_code', base=model, base_vendor_name='anthropic', **codex_pricing)
321
+ _flat_rates('claude_code', model)
321
322
 
322
323
  # %% ../nbs/00_types.ipynb #bb0c4c2a
323
324
  sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
@@ -325,7 +326,7 @@ sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
325
326
  # Its window is smaller than the API's: 371,331 input tokens is accepted and 371,981 is not, on all three.
326
327
  for model in gpt56s:
327
328
  register_model_info(model, 'codex', base=model, base_vendor_name='openai', max_input_tokens=371_000, **codex_pricing)
328
- _rm_ctx_tiers('codex', model)
329
+ _flat_rates('codex', model)
329
330
 
330
331
  # %% ../nbs/00_types.ipynb #24cc47ec
331
332
  def get_model_pricing(mn, vendor_name, million=True):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.48
3
+ Version: 0.0.49
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm