Skip to content

Commit 851e5b6

Browse files
diogoncalvesMiNeves00actions-userbrunoalho99
authored
Develop (#216)
* chore: Provider Unit Tests (#173) * chore: added unit tests for core provider. small bugfix on calculate_metrics of provider * added unit tests and docstring for join chunks * added unit tests and docstrings for calculate_cost on provider * added unit tests and docstrings for input_to_string on provider * added unit tests and docstrings for chat and achat * added unit tests and docstrings for chat and achat * chore: cleaned provider unit tests * chore: separated provider tests into different files. fixed some of its tests * chore: linted code * chore: deleted some comments * chore: linted * chore: Added Azure Provider Unit Tests (#176) * chore: added unit tests for azure provider * chore: added more unit tests and docstrings on azure, removed redundant comments * chore: added unit tests for generate client on Azure Provider * chore: separated azure unit tests into separate files. fixed some of its tests. * chore: linted code * chore: new line Signed-off-by: Diogo Goncalves <diogoncalves@users.noreply.github.com> --------- Signed-off-by: Diogo Goncalves <diogoncalves@users.noreply.github.com> Co-authored-by: Diogo Goncalves <diogoncalves@users.noreply.github.com> * [fix] bump prerelease version in pyproject.toml * chore: rename action * feat: added action to run tests on PR * chore: comments * fix: fix azure config tests * chore: style format * fix: tests workflow * Feature/prompt management (#200) * [feat] prompt management * [feat] testing * [feat] only one active prompt * [fix] bump prerelease version in pyproject.toml * [bugfix] return empty prompt * [fix] bump prerelease version in pyproject.toml * Update CONTRIBUTING.md Signed-off-by: Diogo Goncalves <diogoncalves@users.noreply.github.com> * Feat/ Use Openai Usage to calculate Cache and Reasoning Costs (#199) * feat: collects usage from stream and non stream openai calls * chore: refactored to provider to have a Metrics obj * feat: calculate_metrics now takes into account cached & reasoning tokens. Prices of openai models updated * fix: added caching tokens to model config obj * chore: added integration test for cache and reasoning * chore: added integration test for usage retrieval when max tokens reached * chore: uncommented runs from examples/core.py * fix: bugfix regarding usage on function calling. added a test for this * chore: merged with develop * chore: extracted provider data structures to another file * chore: renamed to private methods some within provider. splitted integration tests into 2 files * chore: deletion of a todo comment * chore: update poetry.lock * chore: specify python versions * chore: moving langchain integration tests to sdk * chore: format * feat: added support for o3-mini and updated o1-mini prices. also updated integration tests to support o3 (#202) * chore: removed duplicated code; removed duplicated integration tests * chore: updated github actions to run integration tests * chore: fixing github actions * chore: fixing github actions again * chore: fixing github actions again-x2 * chore: fixing github actions again-x2 * chore: added cache of dependencies to integration-tests in githubaction * chore: updated integration-tests action to inject github secrets into env * Feat/bedrock support for Nova models through the ConverseAPI (#207) * feat: added support for bedrock nova models * feat: tokens are now read from usage if available to ensure accuracy * chore: removed duplicated integration tests folder in wrong place * feat: refactored bedrock provider into being a single file instead of folder * chore: renamed bedrock to bedrock-converse in examples/core.py * chore: renamed bedrock in config.yaml * [fix] bump prerelease version in pyproject.toml * [fix] bump prerelease version in pyproject.toml * [fix] bump prerelease version in pyproject.toml * Update pyproject.toml updated llmstudio-tracker version Signed-off-by: Miguel Neves <61327611+MiNeves00@users.noreply.github.com> * [fix] bump prerelease version in pyproject.toml * chore: updated llmstudio sdk poetry.lock * Feat/converse support images (#211) * feat: added converse-api support for images in input. started making an integration test for this. * chore: added integration test for converse image sending * chore: send images integration test now also tests for openai * chore: integration test of send_imgs added async testing * chore: updated examples core.py to also have send images * feat: bedrock image input is now same contract as openai * chore: ChatCompletionLLMstudio print now hides large image bytes for readability * chore: fixes in the pretty print of ChatCompletionLLMstudio * chore: small fix in examples/core.py * fix: test_send_imgs had bug on reading env * chore: made clean_print optional on chatcompletions; image from url is directly converted to bytes * [fix] bump prerelease version in pyproject.toml * [fix] bump prerelease version in pyproject.toml * [fix] bump prerelease version in pyproject.toml * Update pyproject.toml Signed-off-by: Diogo Goncalves <diogoncalves@users.noreply.github.com> * Update pyproject.toml Signed-off-by: Diogo Goncalves <diogoncalves@users.noreply.github.com> * chore: format --------- Signed-off-by: Diogo Goncalves <diogoncalves@users.noreply.github.com> Signed-off-by: Miguel Neves <61327611+MiNeves00@users.noreply.github.com> Co-authored-by: Miguel Neves <61327611+MiNeves00@users.noreply.github.com> Co-authored-by: GitHub Actions <actions@github.com> Co-authored-by: brunoalho99 <132477278+brunoalho99@users.noreply.github.com> Co-authored-by: brunoalho <bruno.alho@tensorops.ai> Co-authored-by: Miguel Neves <miguel.neves.filipe@gmail.com>
1 parent cd453f8 commit 851e5b6

File tree

9 files changed

+2220
-1517
lines changed

9 files changed

+2220
-1517
lines changed

examples/core.py

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ def run_provider(provider, model, api_key=None, **kwargs):
1212
print(f"\n\n###RUNNING for <{provider}>, <{model}> ###")
1313
llm = LLMCore(provider=provider, api_key=api_key, **kwargs)
1414

15-
latencies = {}
16-
15+
latencies = {}
1716
print("\nAsync Non-Stream")
18-
chat_request = build_chat_request(model, chat_input="Hello, my name is Jason Json", is_stream=False)
17+
chat_request = build_chat_request(model, chat_input="Hello, my name is Jason", is_stream=False)
1918
string = """
20-
What is Lorem Ipsum? json
19+
What is Lorem Ipsum?
2120
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
2221
2322
Why do we use it?
@@ -27,7 +26,7 @@ def run_provider(provider, model, api_key=None, **kwargs):
2726
Where does it come from?
2827
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
2928
30-
What is Lorem Ipsum? json
29+
What is Lorem Ipsum?
3130
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
3231
3332
Why do we use it?
@@ -37,7 +36,7 @@ def run_provider(provider, model, api_key=None, **kwargs):
3736
Where does it come from?
3837
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
3938
40-
What is Lorem Ipsum? json
39+
What is Lorem Ipsum?
4140
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
4241
4342
Why do we use it?
@@ -50,15 +49,14 @@ def run_provider(provider, model, api_key=None, **kwargs):
5049
"""
5150
#chat_request = build_chat_request(model, chat_input=string, is_stream=False)
5251

53-
5452
response_async = asyncio.run(llm.achat(**chat_request))
5553
pprint(response_async)
5654
latencies["async (ms)"]= response_async.metrics["latency_s"]*1000
5755

5856

5957
print("\nAsync Stream")
6058
async def async_stream():
61-
chat_request = build_chat_request(model, chat_input="Hello, my name is Tom Json", is_stream=True)
59+
chat_request = build_chat_request(model, chat_input="Hello, my name is Tom", is_stream=True)
6260

6361
response_async = await llm.achat(**chat_request)
6462
async for p in response_async:
@@ -74,15 +72,16 @@ async def async_stream():
7472

7573

7674
print("\nSync Non-Stream")
77-
chat_request = build_chat_request(model, chat_input="Hello, my name is Alice Json", is_stream=False)
75+
chat_request = build_chat_request(model, chat_input="Hello, my name is Alice", is_stream=False)
7876

7977
response_sync = llm.chat(**chat_request)
8078
pprint(response_sync)
8179
latencies["sync (ms)"]= response_sync.metrics["latency_s"]*1000
8280

8381

8482
print("\nSync Stream")
85-
chat_request = build_chat_request(model, chat_input="Hello, my name is Mary Json", is_stream=True)
83+
chat_request = build_chat_request(model, chat_input="Hello, my name is Mary", is_stream=True)
84+
8685

8786
response_sync_stream = llm.chat(**chat_request)
8887
for p in response_sync_stream:
@@ -126,7 +125,6 @@ def build_chat_request(model: str, chat_input: str, is_stream: bool, max_tokens:
126125
"parameters": {
127126
"temperature": 0,
128127
"max_tokens": max_tokens,
129-
"response_format": {"type": "json_object"},
130128
"functions": None,
131129
}
132130
}
@@ -137,30 +135,83 @@ def multiple_provider_runs(provider:str, model:str, num_runs:int, api_key:str, *
137135
for _ in range(num_runs):
138136
latencies = run_provider(provider=provider, model=model, api_key=api_key, **kwargs)
139137
pprint(latencies)
140-
141-
142138

143-
# OpenAI
144-
multiple_provider_runs(provider="openai", model="gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"], num_runs=1)
145-
multiple_provider_runs(provider="openai", model="o3-mini", api_key=os.environ["OPENAI_API_KEY"], num_runs=1)
146-
#multiple_provider_runs(provider="openai", model="o1-preview", api_key=os.environ["OPENAI_API_KEY"], num_runs=1)
139+
140+
def run_chat_all_providers():
141+
# OpenAI
142+
multiple_provider_runs(provider="openai", model="gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"], num_runs=1)
143+
multiple_provider_runs(provider="openai", model="o3-mini", api_key=os.environ["OPENAI_API_KEY"], num_runs=1)
144+
#multiple_provider_runs(provider="openai", model="o1-preview", api_key=os.environ["OPENAI_API_KEY"], num_runs=1)
145+
146+
# Azure
147+
multiple_provider_runs(provider="azure", model="gpt-4o-mini", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
148+
#multiple_provider_runs(provider="azure", model="gpt-4o", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
149+
#multiple_provider_runs(provider="azure", model="o1-mini", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
150+
#multiple_provider_runs(provider="azure", model="o1-preview", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
151+
152+
# Azure
153+
multiple_provider_runs(provider="azure", model="gpt-4o-mini", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
154+
#multiple_provider_runs(provider="azure", model="gpt-4o", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
155+
#multiple_provider_runs(provider="azure", model="o1-mini", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
156+
#multiple_provider_runs(provider="azure", model="o1-preview", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
157+
147158

148159

149-
# Azure
150-
multiple_provider_runs(provider="azure", model="gpt-4o-mini", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
151-
#multiple_provider_runs(provider="azure", model="gpt-4o", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
152-
#multiple_provider_runs(provider="azure", model="o1-mini", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
153-
#multiple_provider_runs(provider="azure", model="o1-preview", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
160+
#multiple_provider_runs(provider="anthropic", model="claude-3-opus-20240229", num_runs=1, api_key=os.environ["ANTHROPIC_API_KEY"])
154161

162+
#multiple_provider_runs(provider="azure", model="o1-preview", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
163+
#multiple_provider_runs(provider="azure", model="o1-mini", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
155164

156-
#multiple_provider_runs(provider="anthropic", model="claude-3-opus-20240229", num_runs=1, api_key=os.environ["ANTHROPIC_API_KEY"])
157165

158-
#multiple_provider_runs(provider="azure", model="o1-preview", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
159-
#multiple_provider_runs(provider="azure", model="o1-mini", num_runs=1, api_key=os.environ["AZURE_API_KEY"], api_version=os.environ["AZURE_API_VERSION"], api_endpoint=os.environ["AZURE_API_ENDPOINT"])
166+
multiple_provider_runs(provider="vertexai", model="gemini-1.5-flash", num_runs=1, api_key=os.environ["GOOGLE_API_KEY"])
160167

168+
# Bedrock
169+
multiple_provider_runs(provider="bedrock", model="us.amazon.nova-lite-v1:0", num_runs=1, api_key=None, region=os.environ["BEDROCK_REGION"], secret_key=os.environ["BEDROCK_SECRET_KEY"], access_key=os.environ["BEDROCK_ACCESS_KEY"])
170+
#multiple_provider_runs(provider="bedrock", model="anthropic.claude-3-5-sonnet-20241022-v2:0", num_runs=1, api_key=None, region=os.environ["BEDROCK_REGION"], secret_key=os.environ["BEDROCK_SECRET_KEY"], access_key=os.environ["BEDROCK_ACCESS_KEY"])
161171

162-
multiple_provider_runs(provider="vertexai", model="gemini-1.5-flash", num_runs=1, api_key=os.environ["GOOGLE_API_KEY"])
172+
run_chat_all_providers()
163173

164-
# Bedrock
165-
multiple_provider_runs(provider="bedrock", model="us.amazon.nova-lite-v1:0", num_runs=1, api_key=None, region=os.environ["BEDROCK_REGION"], secret_key=os.environ["BEDROCK_SECRET_KEY"], access_key=os.environ["BEDROCK_ACCESS_KEY"])
166-
#multiple_provider_runs(provider="bedrock", model="anthropic.claude-3-5-sonnet-20241022-v2:0", num_runs=1, api_key=None, region=os.environ["BEDROCK_REGION"], secret_key=os.environ["BEDROCK_SECRET_KEY"], access_key=os.environ["BEDROCK_ACCESS_KEY"])
174+
175+
import base64
176+
177+
def messages(img_path):
178+
"""
179+
Creates a message payload with both text and image.
180+
Adapts format based on the provider.
181+
"""
182+
with open(img_path, "rb") as f:
183+
image_bytes = f.read()
184+
185+
base64_image = base64.b64encode(image_bytes).decode("utf-8")
186+
return [
187+
{
188+
"role": "user",
189+
"content": [
190+
{"type": "text", "text": "What's in this image?"},
191+
{
192+
"type": "image_url",
193+
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
194+
},
195+
{
196+
"type": "image_url",
197+
"image_url": {"url": "https://awsmp-logos.s3.amazonaws.com/seller-zx4pk43qpmxoa/53d235806f343cec94aac3c577d81c13.png"},
198+
},
199+
],
200+
}
201+
]
202+
203+
def run_send_imgs():
204+
provider="bedrock"
205+
model="us.amazon.nova-lite-v1:0"
206+
chat_input=messages(img_path="./libs/llmstudio/tests/integration_tests/test_data/llmstudio-logo.jpeg")
207+
chat_request = build_chat_request(model=model, chat_input=chat_input, is_stream=False)
208+
llm = LLMCore(provider=provider, api_key=os.environ["OPENAI_API_KEY"], region=os.environ["BEDROCK_REGION"], secret_key=os.environ["BEDROCK_SECRET_KEY"], access_key=os.environ["BEDROCK_ACCESS_KEY"])
209+
response_sync = llm.chat(**chat_request)
210+
#print(response_sync)
211+
response_sync.clean_print()
212+
213+
#for p in response_sync:
214+
# if p.metrics:
215+
# p.clean_print()
216+
217+
run_send_imgs()

libs/core/llmstudio_core/providers/bedrock_converse.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import base64
12
import json
23
import os
4+
import re
35
import time
46
import uuid
57
from typing import (
@@ -14,6 +16,7 @@
1416
)
1517

1618
import boto3
19+
import requests
1720
from llmstudio_core.exceptions import ProviderError
1821
from llmstudio_core.providers.provider import ChatRequest, ProviderCore, provider
1922
from llmstudio_core.utils import OpenAIToolFunction
@@ -276,6 +279,34 @@ def _process_messages(
276279
}
277280
)
278281
messages.append(tool_use)
282+
elif isinstance(message.get("content"), list):
283+
converse_content_list = []
284+
for content in message.get("content"):
285+
converse_content = {}
286+
if content.get("type") == "text":
287+
converse_content["text"] = content.get("text")
288+
elif content.get("type") == "image_url":
289+
image_url = content.get("image_url")["url"]
290+
bytes_image = BedrockConverseProvider._get_image_bytes(
291+
image_url
292+
)
293+
format = (
294+
BedrockConverseProvider._get_img_format_from_bytes(
295+
bytes_image
296+
)
297+
)
298+
converse_content["image"] = {
299+
"format": format,
300+
"source": {"bytes": bytes_image},
301+
}
302+
converse_content_list.append(converse_content)
303+
304+
messages.append(
305+
{
306+
"role": message.get("role"),
307+
"content": converse_content_list,
308+
}
309+
)
279310
else:
280311
messages.append(
281312
{
@@ -303,6 +334,62 @@ def _process_messages(
303334

304335
return messages, system_prompt
305336

337+
@staticmethod
338+
def _base64_to_bytes(image_url: str) -> bytes:
339+
"""
340+
Extracts and decodes Base64 image data from a 'data:image/...;base64,...' URL.
341+
Returns the raw image bytes.
342+
"""
343+
if not image_url.startswith("data:image/"):
344+
raise ValueError("Invalid Base64 image URL")
345+
346+
base64_data = re.sub(r"^data:image/[^;]+;base64,", "", image_url)
347+
348+
return base64.b64decode(base64_data)
349+
350+
@staticmethod
351+
def _get_img_format_from_bytes(image_bytes: bytes) -> str:
352+
"""
353+
Determines the image format from raw image bytes using file signatures (magic numbers).
354+
"""
355+
if image_bytes.startswith(b"\xFF\xD8\xFF"):
356+
return "jpeg"
357+
elif image_bytes.startswith(b"\x89PNG\r\n\x1A\n"):
358+
return "png"
359+
elif image_bytes.startswith(b"GIF87a") or image_bytes.startswith(b"GIF89a"):
360+
return "gif"
361+
elif (
362+
image_bytes.startswith(b"\x52\x49\x46\x46") and image_bytes[8:12] == b"WEBP"
363+
):
364+
return "webp"
365+
elif image_bytes.startswith(b"\x49\x49\x2A\x00") or image_bytes.startswith(
366+
b"\x4D\x4D\x00\x2A"
367+
):
368+
return "tiff"
369+
else:
370+
raise ValueError("Unknown image format")
371+
372+
@staticmethod
373+
def _get_image_bytes(image_url: str) -> bytes:
374+
"""
375+
Converts an image URL to a Base64-encoded string.
376+
- If already in 'data:image/...;base64,...' format, it returns as-is.
377+
- If it's a normal URL, downloads and encodes the image in Base64.
378+
"""
379+
if image_url.startswith("data:image/"):
380+
return BedrockConverseProvider._base64_to_bytes(image_url)
381+
382+
elif image_url.startswith(("http://", "https://")):
383+
response = requests.get(image_url)
384+
if response.status_code != 200:
385+
raise ValueError(f"Failed to download image: {response.status_code}")
386+
387+
image_bytes = response.content
388+
return image_bytes
389+
390+
else:
391+
raise ValueError("Invalid image URL format")
392+
306393
@staticmethod
307394
def _process_tools(parameters: dict) -> Optional[Dict]:
308395
if parameters.get("tools") is None and parameters.get("functions") is None:

0 commit comments

Comments
 (0)