Files
2026-07-13 12:38:14 +08:00

1287 lines
42 KiB
Python

import backoff
import dspy
import functools
import logging
import os
import random
import requests
import threading
from typing import Optional, Literal, Any
import ujson
from pathlib import Path
from dsp import ERRORS, backoff_hdlr, giveup_hdlr
from dsp.modules.hf import openai_to_hf
from dsp.modules.hf_client import send_hftgi_request_v01_wrapped
from openai import OpenAI, AzureOpenAI
from transformers import AutoTokenizer
try:
from anthropic import RateLimitError
except ImportError:
RateLimitError = None
############################
# Code copied from https://github.com/stanfordnlp/dspy/blob/main/dspy/clients/lm.py on Sep 29, 2024
# try:
import warnings
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning)
if "LITELLM_LOCAL_MODEL_COST_MAP" not in os.environ:
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
import litellm
litellm.drop_params = True
litellm.telemetry = False
from litellm.caching.caching import Cache
disk_cache_dir = os.path.join(Path.home(), ".storm_local_cache")
litellm.cache = Cache(disk_cache_dir=disk_cache_dir, type="disk")
# except ImportError:
# class LitellmPlaceholder:
# def __getattr__(self, _):
# raise ImportError(
# "The LiteLLM package is not installed. Run `pip install litellm`."
# )
# litellm = LitellmPlaceholder()
LM_LRU_CACHE_MAX_SIZE = 3000
class LM:
def __init__(
self,
model,
model_type="chat",
temperature=0.0,
max_tokens=1000,
cache=True,
**kwargs,
):
self.model = model
self.model_type = model_type
self.cache = cache
self.kwargs = dict(temperature=temperature, max_tokens=max_tokens, **kwargs)
self.history = []
if "o1-" in model:
assert (
max_tokens >= 5000 and temperature == 1.0
), "OpenAI's o1-* models require passing temperature=1.0 and max_tokens >= 5000 to `dspy.LM(...)`"
def __call__(self, prompt=None, messages=None, **kwargs):
# Build the request.
cache = kwargs.pop("cache", self.cache)
messages = messages or [{"role": "user", "content": prompt}]
kwargs = {**self.kwargs, **kwargs}
# Make the request and handle LRU & disk caching.
if self.model_type == "chat":
completion = cached_litellm_completion if cache else litellm_completion
else:
completion = (
cached_litellm_text_completion if cache else litellm_text_completion
)
response = completion(
ujson.dumps(dict(model=self.model, messages=messages, **kwargs))
)
outputs = [
c.message.content if hasattr(c, "message") else c["text"]
for c in response["choices"]
]
# Logging, with removed api key & where `cost` is None on cache hit.
kwargs = {k: v for k, v in kwargs.items() if not k.startswith("api_")}
entry = dict(prompt=prompt, messages=messages, kwargs=kwargs, response=response)
entry = dict(**entry, outputs=outputs, usage=dict(response["usage"]))
entry = dict(
**entry, cost=response.get("_hidden_params", {}).get("response_cost")
)
self.history.append(entry)
return outputs
def inspect_history(self, n: int = 1):
_inspect_history(self, n)
@functools.lru_cache(maxsize=LM_LRU_CACHE_MAX_SIZE)
def cached_litellm_completion(request):
return litellm_completion(request, cache={"no-cache": False, "no-store": False})
def litellm_completion(request, cache={"no-cache": True, "no-store": True}):
kwargs = ujson.loads(request)
return litellm.completion(cache=cache, **kwargs)
@functools.lru_cache(maxsize=LM_LRU_CACHE_MAX_SIZE)
def cached_litellm_text_completion(request):
return litellm_text_completion(
request, cache={"no-cache": False, "no-store": False}
)
def litellm_text_completion(request, cache={"no-cache": True, "no-store": True}):
kwargs = ujson.loads(request)
# Extract the provider and model from the model string.
model = kwargs.pop("model").split("/", 1)
provider, model = model[0] if len(model) > 1 else "openai", model[-1]
# Use the API key and base from the kwargs, or from the environment.
api_key = kwargs.pop("api_key", None) or os.getenv(f"{provider}_API_KEY")
api_base = kwargs.pop("api_base", None) or os.getenv(f"{provider}_API_BASE")
# Build the prompt from the messages.
prompt = "\n\n".join(
[x["content"] for x in kwargs.pop("messages")] + ["BEGIN RESPONSE:"]
)
return litellm.text_completion(
cache=cache,
model=f"text-completion-openai/{model}",
api_key=api_key,
api_base=api_base,
prompt=prompt,
**kwargs,
)
def _green(text: str, end: str = "\n"):
return "\x1b[32m" + str(text).lstrip() + "\x1b[0m" + end
def _red(text: str, end: str = "\n"):
return "\x1b[31m" + str(text) + "\x1b[0m" + end
def _inspect_history(lm, n: int = 1):
"""Prints the last n prompts and their completions."""
for item in lm.history[-n:]:
messages = item["messages"] or [{"role": "user", "content": item["prompt"]}]
outputs = item["outputs"]
print("\n\n\n")
for msg in messages:
print(_red(f"{msg['role'].capitalize()} message:"))
print(msg["content"].strip())
print("\n")
print(_red("Response:"))
print(_green(outputs[0].strip()))
if len(outputs) > 1:
choices_text = f" \t (and {len(outputs)-1} other completions)"
print(_red(choices_text, end=""))
print("\n\n\n")
############################
class LitellmModel(LM):
"""A wrapper class for LiteLLM.
Check out https://docs.litellm.ai/docs/providers for usage details.
"""
def __init__(
self,
model: str = "openai/gpt-4o-mini",
api_key: Optional[str] = None,
model_type: Literal["chat", "text"] = "chat",
**kwargs,
):
super().__init__(model=model, api_key=api_key, model_type=model_type, **kwargs)
self._token_usage_lock = threading.Lock()
self.prompt_tokens = 0
self.completion_tokens = 0
def log_usage(self, response):
"""Log the total tokens from the OpenAI API response."""
usage_data = response.get("usage")
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.get("prompt_tokens", 0)
self.completion_tokens += usage_data.get("completion_tokens", 0)
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.model
or self.kwargs.get("model")
or self.kwargs.get("engine"): {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
def __call__(self, prompt=None, messages=None, **kwargs):
# Build the request.
cache = kwargs.pop("cache", self.cache)
messages = messages or [{"role": "user", "content": prompt}]
kwargs = {**self.kwargs, **kwargs}
# Make the request and handle LRU & disk caching.
if self.model_type == "chat":
completion = cached_litellm_completion if cache else litellm_completion
else:
completion = (
cached_litellm_text_completion if cache else litellm_text_completion
)
response = completion(
ujson.dumps(dict(model=self.model, messages=messages, **kwargs))
)
response_dict = response.json()
self.log_usage(response_dict)
outputs = [
c.message.content if hasattr(c, "message") else c["text"]
for c in response["choices"]
]
# Logging, with removed api key & where `cost` is None on cache hit.
kwargs = {k: v for k, v in kwargs.items() if not k.startswith("api_")}
entry = dict(
prompt=prompt, messages=messages, kwargs=kwargs, response=response_dict
)
entry = dict(**entry, outputs=outputs, usage=dict(response_dict["usage"]))
entry = dict(
**entry, cost=response.get("_hidden_params", {}).get("response_cost")
)
self.history.append(entry)
return outputs
# ========================================================================
# The following language model classes were deprecated after v1.1.0.
# They remain in this file for backward compatibility but will no longer be maintained.
class OpenAIModel(dspy.OpenAI):
"""A wrapper class for dspy.OpenAI."""
def __init__(
self,
model: str = "gpt-4o-mini",
api_key: Optional[str] = None,
model_type: Literal["chat", "text"] = None,
**kwargs,
):
super().__init__(model=model, api_key=api_key, model_type=model_type, **kwargs)
self._token_usage_lock = threading.Lock()
self.prompt_tokens = 0
self.completion_tokens = 0
def log_usage(self, response):
"""Log the total tokens from the OpenAI API response."""
usage_data = response.get("usage")
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.get("prompt_tokens", 0)
self.completion_tokens += usage_data.get("completion_tokens", 0)
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.kwargs.get("model")
or self.kwargs.get("engine"): {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
def __call__(
self,
prompt: str,
only_completed: bool = True,
return_sorted: bool = False,
**kwargs,
) -> list[dict[str, Any]]:
"""Copied from dspy/dsp/modules/gpt3.py with the addition of tracking token usage."""
assert only_completed, "for now"
assert return_sorted is False, "for now"
# if kwargs.get("n", 1) > 1:
# if self.model_type == "chat":
# kwargs = {**kwargs}
# else:
# kwargs = {**kwargs, "logprobs": 5}
response = self.request(prompt, **kwargs)
# Log the token usage from the OpenAI API response.
self.log_usage(response)
choices = response["choices"]
completed_choices = [c for c in choices if c["finish_reason"] != "length"]
if only_completed and len(completed_choices):
choices = completed_choices
completions = [self._get_choice_text(c) for c in choices]
if return_sorted and kwargs.get("n", 1) > 1:
scored_completions = []
for c in choices:
tokens, logprobs = (
c["logprobs"]["tokens"],
c["logprobs"]["token_logprobs"],
)
if "<|endoftext|>" in tokens:
index = tokens.index("<|endoftext|>") + 1
tokens, logprobs = tokens[:index], logprobs[:index]
avglog = sum(logprobs) / len(logprobs)
scored_completions.append((avglog, self._get_choice_text(c)))
scored_completions = sorted(scored_completions, reverse=True)
completions = [c for _, c in scored_completions]
return completions
class DeepSeekModel(dspy.OpenAI):
"""A wrapper class for DeepSeek API, compatible with dspy.OpenAI."""
def __init__(
self,
model: str = "deepseek-chat",
api_key: Optional[str] = None,
api_base: str = "https://api.deepseek.com",
**kwargs,
):
super().__init__(model=model, api_key=api_key, api_base=api_base, **kwargs)
self._token_usage_lock = threading.Lock()
self.prompt_tokens = 0
self.completion_tokens = 0
self.model = model
self.api_key = api_key or os.getenv("DEEPSEEK_API_KEY")
self.api_base = api_base
if not self.api_key:
raise ValueError(
"DeepSeek API key must be provided either as an argument or as an environment variable DEEPSEEK_API_KEY"
)
def log_usage(self, response):
"""Log the total tokens from the DeepSeek API response."""
usage_data = response.get("usage")
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.get("prompt_tokens", 0)
self.completion_tokens += usage_data.get("completion_tokens", 0)
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.model: {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
@backoff.on_exception(
backoff.expo,
ERRORS,
max_time=1000,
on_backoff=backoff_hdlr,
giveup=giveup_hdlr,
)
def _create_completion(self, prompt: str, **kwargs):
"""Create a completion using the DeepSeek API."""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
data = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
**kwargs,
}
response = requests.post(
f"{self.api_base}/v1/chat/completions", headers=headers, json=data
)
response.raise_for_status()
return response.json()
def __call__(
self,
prompt: str,
only_completed: bool = True,
return_sorted: bool = False,
**kwargs,
) -> list[dict[str, Any]]:
"""Call the DeepSeek API to generate completions."""
assert only_completed, "for now"
assert return_sorted is False, "for now"
response = self._create_completion(prompt, **kwargs)
# Log the token usage from the DeepSeek API response.
self.log_usage(response)
choices = response["choices"]
completions = [choice["message"]["content"] for choice in choices]
history = {
"prompt": prompt,
"response": response,
"kwargs": kwargs,
}
self.history.append(history)
return completions
class AzureOpenAIModel(dspy.LM):
"""A wrapper class of Azure OpenAI endpoint.
Note: param::model should match the deployment_id on your Azure platform.
"""
def __init__(
self,
azure_endpoint: str,
api_version: str,
model: str,
api_key: str,
model_type: Literal["chat", "text"] = "chat",
**kwargs,
):
super().__init__(model=model)
self._token_usage_lock = threading.Lock()
self.prompt_tokens = 0
self.completion_tokens = 0
self.model = model
self.provider = "azure"
self.model_type = model_type
self.client = AzureOpenAI(
azure_endpoint=azure_endpoint,
api_key=api_key,
api_version=api_version,
)
self.prompt_tokens = 0
self.completion_tokens = 0
self.kwargs = {
"model": model,
"temperature": 0.0,
"max_tokens": 150,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
"n": 1,
**kwargs,
}
@backoff.on_exception(
backoff.expo,
ERRORS,
max_time=1000,
on_backoff=backoff_hdlr,
giveup=giveup_hdlr,
)
def basic_request(self, prompt: str, **kwargs) -> Any:
kwargs = {**self.kwargs, **kwargs}
try:
if self.model_type == "chat":
messages = [{"role": "user", "content": prompt}]
response = self.client.chat.completions.create(
messages=messages, **kwargs
)
else:
response = self.client.completions.create(prompt=prompt, **kwargs)
self.log_usage(response)
history_entry = {
"prompt": prompt,
"response": dict(response),
"kwargs": kwargs,
}
self.history.append(history_entry)
return response
except Exception as e:
logging.error(f"Error making request to Azure OpenAI: {str(e)}")
raise
def _get_choice_text(self, choice: Any) -> str:
"""Extract text from a choice object based on model type."""
if self.model_type == "chat":
return choice.message.content
return choice.text
def log_usage(self, response):
"""Log the total tokens from response."""
usage_data = response.usage
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.prompt_tokens
self.completion_tokens += usage_data.completion_tokens
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.model: {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
def __call__(
self,
prompt: str,
only_completed: bool = True,
return_sorted: bool = False,
**kwargs,
) -> list[str]:
"""Get completions from Azure OpenAI.
Args:
prompt: The prompt to send to the model
only_completed: Only return completed responses
return_sorted: Sort completions by probability (not implemented)
**kwargs: Additional arguments to pass to the API
Returns:
List of completion strings
"""
response = self.basic_request(prompt, **kwargs)
choices = response.choices
completed_choices = [c for c in choices if c.finish_reason != "length"]
if only_completed and completed_choices:
choices = completed_choices
completions = [self._get_choice_text(c) for c in choices]
return completions
class GroqModel(dspy.OpenAI):
"""A wrapper class for Groq API (https://console.groq.com/), compatible with dspy.OpenAI."""
def __init__(
self,
model: str = "llama3-70b-8192",
api_key: Optional[str] = None,
api_base: str = "https://api.groq.com/openai/v1",
**kwargs,
):
super().__init__(model=model, api_key=api_key, api_base=api_base, **kwargs)
self._token_usage_lock = threading.Lock()
self.prompt_tokens = 0
self.completion_tokens = 0
self.model = model
self.api_key = api_key or os.getenv("GROQ_API_KEY")
self.api_base = api_base
if not self.api_key:
raise ValueError(
"Groq API key must be provided either as an argument or as an environment variable GROQ_API_KEY"
)
def log_usage(self, response):
"""Log the total tokens from the Groq API response."""
usage_data = response.get("usage")
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.get("prompt_tokens", 0)
self.completion_tokens += usage_data.get("completion_tokens", 0)
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.model: {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
@backoff.on_exception(
backoff.expo,
ERRORS,
max_time=1000,
on_backoff=backoff_hdlr,
giveup=giveup_hdlr,
)
def _create_completion(self, prompt: str, **kwargs):
"""Create a completion using the Groq API."""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
# Remove unsupported fields
kwargs.pop("logprobs", None)
kwargs.pop("logit_bias", None)
kwargs.pop("top_logprobs", None)
# Ensure N is 1 if supplied
if "n" in kwargs and kwargs["n"] != 1:
raise ValueError("Groq API only supports N=1")
# Adjust temperature if it's 0
if kwargs.get("temperature", 1) == 0:
kwargs["temperature"] = 1e-8
data = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
**kwargs,
}
# Remove 'name' field from messages if present
for message in data["messages"]:
message.pop("name", None)
response = requests.post(
f"{self.api_base}/chat/completions", headers=headers, json=data
)
response.raise_for_status()
return response.json()
def __call__(
self,
prompt: str,
only_completed: bool = True,
return_sorted: bool = False,
**kwargs,
) -> list[dict[str, Any]]:
"""Call the Groq API to generate completions."""
assert only_completed, "for now"
assert return_sorted is False, "for now"
response = self._create_completion(prompt, **kwargs)
# Log the token usage from the Groq API response.
self.log_usage(response)
choices = response["choices"]
completions = [choice["message"]["content"] for choice in choices]
history = {
"prompt": prompt,
"response": response,
"kwargs": kwargs,
}
self.history.append(history)
return completions
class ClaudeModel(dspy.dsp.modules.lm.LM):
"""Copied from dspy/dsp/modules/anthropic.py with the addition of tracking token usage."""
def __init__(
self,
model: str,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
**kwargs,
):
super().__init__(model)
try:
from anthropic import Anthropic
except ImportError as err:
raise ImportError("Claude requires `pip install anthropic`.") from err
self.provider = "anthropic"
self.api_key = api_key = (
os.environ.get("ANTHROPIC_API_KEY") if api_key is None else api_key
)
self.api_base = (
"https://api.anthropic.com/v1/messages" if api_base is None else api_base
)
self.kwargs = {
"temperature": kwargs.get("temperature", 0.0),
"max_tokens": min(kwargs.get("max_tokens", 4096), 4096),
"top_p": kwargs.get("top_p", 1.0),
"top_k": kwargs.get("top_k", 1),
"n": kwargs.pop("n", kwargs.pop("num_generations", 1)),
**kwargs,
"model": model,
}
self.history: list[dict[str, Any]] = []
self.client = Anthropic(api_key=api_key)
self.model = model
self._token_usage_lock = threading.Lock()
self.prompt_tokens = 0
self.completion_tokens = 0
def log_usage(self, response):
"""Log the total tokens from the Anthropic API response."""
usage_data = response.usage
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.input_tokens
self.completion_tokens += usage_data.output_tokens
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.model: {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
def basic_request(self, prompt: str, **kwargs):
raw_kwargs = kwargs
kwargs = {**self.kwargs, **kwargs}
# caching mechanism requires hashable kwargs
kwargs["messages"] = [{"role": "user", "content": prompt}]
kwargs.pop("n")
response = self.client.messages.create(**kwargs)
# history = {
# "prompt": prompt,
# "response": response,
# "kwargs": kwargs,
# "raw_kwargs": raw_kwargs,
# }
json_serializable_history = {
"prompt": prompt,
"response": {
"content": response.content[0].text,
"model": response.model,
"role": response.role,
"stop_reason": response.stop_reason,
"stop_sequence": response.stop_sequence,
"type": response.type,
"usage": {
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
},
},
"kwargs": kwargs,
"raw_kwargs": raw_kwargs,
}
self.history.append(json_serializable_history)
return response
@backoff.on_exception(
backoff.expo,
(RateLimitError,),
max_time=1000,
max_tries=8,
on_backoff=backoff_hdlr,
giveup=giveup_hdlr,
)
def request(self, prompt: str, **kwargs):
"""Handles retrieval of completions from Anthropic whilst handling API errors."""
return self.basic_request(prompt, **kwargs)
def __call__(self, prompt, only_completed=True, return_sorted=False, **kwargs):
"""Retrieves completions from Anthropic.
Args:
prompt (str): prompt to send to Anthropic
only_completed (bool, optional): return only completed responses and ignores completion due to length. Defaults to True.
return_sorted (bool, optional): sort the completion choices using the returned probabilities. Defaults to False.
Returns:
list[str]: list of completion choices
"""
assert only_completed, "for now"
assert return_sorted is False, "for now"
# per eg here: https://docs.anthropic.com/claude/reference/messages-examples
# max tokens can be used as a proxy to return smaller responses
# so this cannot be a proper indicator for incomplete response unless it isnt the user-intent.
n = kwargs.pop("n", 1)
completions = []
for _ in range(n):
response = self.request(prompt, **kwargs)
self.log_usage(response)
# This is the original behavior in dspy/dsp/modules/anthropic.py.
# Comment it out because it can cause "IndexError: list index out of range" silently
# which is not transparent to developers.
# if only_completed and response.stop_reason == "max_tokens":
# continue
completions = [c.text for c in response.content]
return completions
class VLLMClient(dspy.dsp.LM):
"""A client compatible with vLLM HTTP server.
vLLM HTTP server is designed to be compatible with the OpenAI API. Use OpenAI client to interact with the server.
"""
def __init__(
self,
model,
port,
model_type: Literal["chat", "text"] = "text",
url="http://localhost",
api_key="null",
**kwargs,
):
"""Check out https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html for more information."""
super().__init__(model=model)
# Store additional kwargs for the generate method.
self.kwargs = {**self.kwargs, **kwargs}
self.model = model
self.base_url = f"{url}:{port}/v1/"
if model_type == "chat":
self.base_url += "chat/"
self.client = OpenAI(base_url=self.base_url, api_key=api_key)
self.prompt_tokens = 0
self.completion_tokens = 0
self._token_usage_lock = threading.Lock()
def basic_request(self, prompt, **kwargs):
completion = self.client.chat.completions.create(
**kwargs,
messages=[{"role": "user", "content": prompt}],
)
return completion
@backoff.on_exception(
backoff.expo,
ERRORS,
max_time=1000,
on_backoff=backoff_hdlr,
)
def request(self, prompt: str, **kwargs):
return self.basic_request(prompt, **kwargs)
def log_usage(self, response):
"""Log the total tokens from the response."""
usage_data = response.usage
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.prompt_tokens
self.completion_tokens += usage_data.completion_tokens
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.kwargs.get("model")
or self.kwargs.get("engine"): {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
def __call__(self, prompt: str, **kwargs):
kwargs = {**self.kwargs, **kwargs}
try:
response = self.request(prompt, **kwargs)
except Exception as e:
print(f"Failed to generate completion: {e}")
raise Exception(e)
self.log_usage(response)
choices = response.choices
completions = [choice.message.content for choice in choices]
history = {
"prompt": prompt,
"response": response,
"kwargs": kwargs,
}
self.history.append(history)
return completions
class OllamaClient(dspy.OllamaLocal):
"""A wrapper class for dspy.OllamaClient."""
def __init__(self, model, port, url="http://localhost", **kwargs):
"""Copied from dspy/dsp/modules/hf_client.py with the addition of storing additional kwargs."""
# Check if the URL has 'http://' or 'https://'
if not url.startswith("http://") and not url.startswith("https://"):
url = "http://" + url
super().__init__(model=model, base_url=f"{url}:{port}", **kwargs)
# Store additional kwargs for the generate method.
self.kwargs = {**self.kwargs, **kwargs}
class TGIClient(dspy.HFClientTGI):
def __init__(self, model, port, url, http_request_kwargs=None, **kwargs):
super().__init__(
model=model,
port=port,
url=url,
http_request_kwargs=http_request_kwargs,
**kwargs,
)
def _generate(self, prompt, **kwargs):
"""Copied from dspy/dsp/modules/hf_client.py with the addition of removing hard-coded parameters."""
kwargs = {**self.kwargs, **kwargs}
payload = {
"inputs": prompt,
"parameters": {
"do_sample": kwargs["n"] > 1,
"best_of": kwargs["n"],
"details": kwargs["n"] > 1,
**kwargs,
},
}
payload["parameters"] = openai_to_hf(**payload["parameters"])
# Comment out the following lines to remove the hard-coded parameters.
# payload["parameters"]["temperature"] = max(
# 0.1, payload["parameters"]["temperature"],
# )
response = send_hftgi_request_v01_wrapped(
f"{self.url}:{random.Random().choice(self.ports)}" + "/generate",
url=self.url,
ports=tuple(self.ports),
json=payload,
headers=self.headers,
**self.http_request_kwargs,
)
try:
json_response = response.json()
# completions = json_response["generated_text"]
completions = [json_response["generated_text"]]
if (
"details" in json_response
and "best_of_sequences" in json_response["details"]
):
completions += [
x["generated_text"]
for x in json_response["details"]["best_of_sequences"]
]
response = {"prompt": prompt, "choices": [{"text": c} for c in completions]}
return response
except Exception:
print("Failed to parse JSON response:", response.text)
raise Exception("Received invalid JSON response from server")
class TogetherClient(dspy.HFModel):
"""A wrapper class for dspy.Together."""
def __init__(
self,
model,
api_key: Optional[str] = None,
apply_tokenizer_chat_template=False,
hf_tokenizer_name=None,
model_type: Literal["chat", "text"] = "chat",
**kwargs,
):
"""Copied from dspy/dsp/modules/hf_client.py with the support of applying tokenizer chat template."""
super().__init__(model=model, is_client=True)
self.session = requests.Session()
self.api_key = api_key = (
os.environ.get("TOGETHER_API_KEY") if api_key is None else api_key
)
self.model = model
self.model_type = model_type
if os.getenv("TOGETHER_API_BASE") is None:
if self.model_type == "chat":
self.api_base = "https://api.together.xyz/v1/chat/completions"
else:
self.api_base = "https://api.together.xyz/v1/completions"
else:
self.api_base = os.getenv("TOGETHER_API_BASE")
# self.use_inst_template = False
# if any(keyword in self.model.lower() for keyword in ["inst", "instruct"]):
# self.use_inst_template = True
self.apply_tokenizer_chat_template = apply_tokenizer_chat_template
if self.apply_tokenizer_chat_template:
logging.info("Loading huggingface tokenizer.")
if hf_tokenizer_name is None:
hf_tokenizer_name = self.model
self.tokenizer = AutoTokenizer.from_pretrained(
hf_tokenizer_name, cache_dir=kwargs.get("cache_dir", None)
)
stop_default = "\n\n---"
self.kwargs = {
"temperature": kwargs.get("temperature", 0.0),
"max_tokens": min(kwargs.get("max_tokens", 4096), 4096),
"top_p": kwargs.get("top_p", 1.0),
"top_k": kwargs.get("top_k", 1),
"repetition_penalty": 1,
"n": kwargs.pop("n", kwargs.pop("num_generations", 1)),
"stop": stop_default if "stop" not in kwargs else kwargs["stop"],
**kwargs,
}
self._token_usage_lock = threading.Lock()
self.prompt_tokens = 0
self.completion_tokens = 0
def log_usage(self, response):
"""Log the total tokens from the OpenAI API response."""
usage_data = response.get("usage")
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.get("prompt_tokens", 0)
self.completion_tokens += usage_data.get("completion_tokens", 0)
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.model: {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
@backoff.on_exception(
backoff.expo,
ERRORS,
max_time=1000,
on_backoff=backoff_hdlr,
)
def _generate(self, prompt, **kwargs):
kwargs = {**self.kwargs, **kwargs}
stop = kwargs.get("stop")
temperature = kwargs.get("temperature")
max_tokens = kwargs.get("max_tokens", 150)
top_p = kwargs.get("top_p", 0.7)
top_k = kwargs.get("top_k", 50)
repetition_penalty = kwargs.get("repetition_penalty", 1)
if self.apply_tokenizer_chat_template:
prompt = self.tokenizer.apply_chat_template(
[{"role": "user", "content": prompt}], tokenize=False
)
# prompt = f"[INST]{prompt}[/INST]" if self.use_inst_template else prompt
if self.model_type == "chat":
messages = [
{
"role": "system",
"content": "You are a helpful assistant. You must continue the user text directly without *any* additional interjections.",
},
{"role": "user", "content": prompt},
]
body = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"top_p": top_p,
"top_k": top_k,
"repetition_penalty": repetition_penalty,
"stop": stop,
}
else:
body = {
"model": self.model,
"prompt": prompt,
"temperature": temperature,
"max_tokens": max_tokens,
"top_p": top_p,
"top_k": top_k,
"repetition_penalty": repetition_penalty,
"stop": stop,
}
headers = {"Authorization": f"Bearer {self.api_key}"}
with self.session.post(self.api_base, headers=headers, json=body) as resp:
resp_json = resp.json()
# Log the token usage from the Together API response.
self.log_usage(resp_json)
if self.model_type == "chat":
# completions = [resp_json['output'].get('choices', [])[0].get('message', {}).get('content', "")]
completions = [
resp_json.get("choices", [])[0]
.get("message", {})
.get("content", "")
]
else:
# completions = [resp_json['output'].get('choices', [])[0].get('text', "")]
completions = [resp_json.get("choices", [])[0].get("text", "")]
response = {"prompt": prompt, "choices": [{"text": c} for c in completions]}
return response
class GoogleModel(dspy.dsp.modules.lm.LM):
"""A wrapper class for Google Gemini API."""
def __init__(
self,
model: str,
api_key: Optional[str] = None,
**kwargs,
):
"""You can use `genai.list_models()` to get a list of available models."""
super().__init__(model)
try:
import google.generativeai as genai
except ImportError as err:
raise ImportError(
"GoogleModel requires `pip install google-generativeai`."
) from err
api_key = os.environ.get("GOOGLE_API_KEY") if api_key is None else api_key
genai.configure(api_key=api_key)
kwargs = {
"candidate_count": 1, # Caveat: Gemini API supports only one candidate for now.
"temperature": (
0.0 if "temperature" not in kwargs else kwargs["temperature"]
),
"max_output_tokens": kwargs["max_tokens"],
"top_p": 1,
"top_k": 1,
**kwargs,
}
kwargs.pop("max_tokens", None) # GenerationConfig cannot accept max_tokens
self.model = model
self.config = genai.GenerationConfig(**kwargs)
self.llm = genai.GenerativeModel(
model_name=model, generation_config=self.config
)
self.kwargs = {
"n": 1,
**kwargs,
}
self.history: list[dict[str, Any]] = []
self._token_usage_lock = threading.Lock()
self.prompt_tokens = 0
self.completion_tokens = 0
def log_usage(self, response):
"""Log the total tokens from the Google API response."""
usage_data = response.usage_metadata
if usage_data:
with self._token_usage_lock:
self.prompt_tokens += usage_data.prompt_token_count
self.completion_tokens += usage_data.candidates_token_count
def get_usage_and_reset(self):
"""Get the total tokens used and reset the token usage."""
usage = {
self.model: {
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
}
}
self.prompt_tokens = 0
self.completion_tokens = 0
return usage
def basic_request(self, prompt: str, **kwargs):
raw_kwargs = kwargs
kwargs = {
**self.kwargs,
**kwargs,
}
# Google disallows "n" arguments.
n = kwargs.pop("n", None)
response = self.llm.generate_content(prompt, generation_config=kwargs)
history = {
"prompt": prompt,
"response": [response.to_dict()],
"kwargs": kwargs,
"raw_kwargs": raw_kwargs,
}
self.history.append(history)
return response
@backoff.on_exception(
backoff.expo,
(Exception,),
max_time=1000,
max_tries=8,
on_backoff=backoff_hdlr,
giveup=giveup_hdlr,
)
def request(self, prompt: str, **kwargs):
"""Handles retrieval of completions from Google whilst handling API errors"""
return self.basic_request(prompt, **kwargs)
def __call__(
self,
prompt: str,
only_completed: bool = True,
return_sorted: bool = False,
**kwargs,
):
assert only_completed, "for now"
assert return_sorted is False, "for now"
n = kwargs.pop("n", 1)
completions = []
for _ in range(n):
response = self.request(prompt, **kwargs)
self.log_usage(response)
completions.append(response.parts[0].text)
return completions
# ========================================================================