From f70feb180699aef63d2d99e3207d7fb8f2bae728 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 09:33:54 -0800 Subject: [PATCH 01/33] (test) name with claude-3 --- litellm/tests/test_completion.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index 4db664dde3..e54617bd95 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -83,12 +83,13 @@ def test_completion_claude(): def test_completion_claude_3_empty_response(): + litellm.set_verbose = True messages = [ { "role": "system", "content": "You are 2twNLGfqk4GMOn3ffp4p.", }, - {"role": "user", "content": "Hi gm!"}, + {"role": "user", "content": "Hi gm!", "name": "ishaan"}, {"role": "assistant", "content": "Good morning! How are you doing today?"}, { "role": "user", From 96e369613882a7e6429fc29709f3649a2ff5bca8 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 09:41:58 -0800 Subject: [PATCH 02/33] (fix) support name on perplexity/ --- litellm/llms/openai.py | 24 ++++++++++++++++-------- litellm/llms/prompt_templates/factory.py | 13 +++++++++++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/litellm/llms/openai.py b/litellm/llms/openai.py index 90846b627b..de605edff0 100644 --- a/litellm/llms/openai.py +++ b/litellm/llms/openai.py @@ -237,14 +237,22 @@ class OpenAIChatCompletion(BaseLLM): status_code=422, message=f"Timeout needs to be a float" ) - if custom_llm_provider == "mistral": - # check if message content passed in as list, and not string - messages = prompt_factory( - model=model, - messages=messages, - custom_llm_provider=custom_llm_provider, - ) - + if custom_llm_provider != "openai": + # process all OpenAI compatible provider logic here + if custom_llm_provider == "mistral": + # check if message content passed in as list, and not string + messages = prompt_factory( + model=model, + messages=messages, + custom_llm_provider=custom_llm_provider, + ) + if custom_llm_provider == "perplexity" and messages is not None: + # check if messages.name is passed + supported, if not supported remove + messages = prompt_factory( + model=model, + messages=messages, + custom_llm_provider=custom_llm_provider, + ) for _ in range( 2 ): # if call fails due to alternating messages, retry with reformatted message diff --git a/litellm/llms/prompt_templates/factory.py b/litellm/llms/prompt_templates/factory.py index 616833a2ec..a13130c62a 100644 --- a/litellm/llms/prompt_templates/factory.py +++ b/litellm/llms/prompt_templates/factory.py @@ -556,6 +556,7 @@ def anthropic_messages_pt(messages: list): 3. Each message must alternate between "user" and "assistant" (this is not addressed as now by litellm) 4. final assistant content cannot end with trailing whitespace (anthropic raises an error otherwise) 5. System messages are a separate param to the Messages API (used for tool calling) + 6. Ensure we only accept role, content. (message.name is not supported) """ ## Ensure final assistant message has no trailing whitespace last_assistant_message_idx: Optional[int] = None @@ -583,7 +584,9 @@ def anthropic_messages_pt(messages: list): new_content.append({"type": "text", "text": m["text"]}) new_messages.append({"role": messages[0]["role"], "content": new_content}) # type: ignore else: - new_messages.append(messages[0]) + new_messages.append( + {"role": messages[0]["role"], "content": messages[0]["content"]} + ) return new_messages @@ -606,7 +609,9 @@ def anthropic_messages_pt(messages: list): new_content.append({"type": "text", "content": m["text"]}) new_messages.append({"role": messages[i]["role"], "content": new_content}) # type: ignore else: - new_messages.append(messages[i]) + new_messages.append( + {"role": messages[i]["role"], "content": messages[i]["content"]} + ) if messages[i]["role"] == messages[i + 1]["role"]: if messages[i]["role"] == "user": @@ -897,6 +902,10 @@ def prompt_factory( return anthropic_pt(messages=messages) elif "mistral." in model: return mistral_instruct_pt(messages=messages) + elif custom_llm_provider == "perplexity": + for message in messages: + message.pop("name", None) + return messages try: if "meta-llama/llama-2" in model and "chat" in model: return llama_2_chat_pt(messages=messages) From a6930454604fe6ffef6b9016f43278e6b01cf83c Mon Sep 17 00:00:00 2001 From: Guillermo Date: Fri, 8 Mar 2024 20:33:47 +0100 Subject: [PATCH 03/33] docs: fix yaml typo in proxy/configs.md equals was used instead of : as key-value delimiter in yaml --- docs/my-website/docs/proxy/configs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/my-website/docs/proxy/configs.md b/docs/my-website/docs/proxy/configs.md index 5745685de8..a863ec2cac 100644 --- a/docs/my-website/docs/proxy/configs.md +++ b/docs/my-website/docs/proxy/configs.md @@ -49,9 +49,9 @@ model_list: rpm: 6 - model_name: anthropic-claude litellm_params: - model="bedrock/anthropic.claude-instant-v1" + model: bedrock/anthropic.claude-instant-v1 ### [OPTIONAL] SET AWS REGION ### - aws_region_name="us-east-1" + aws_region_name: us-east-1 - model_name: vllm-models litellm_params: model: openai/facebook/opt-125m # the `openai/` prefix tells litellm it's openai compatible From 2aafbe390b80748edbd67e31935af17375a21dc2 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 13:16:12 -0800 Subject: [PATCH 04/33] (feat) read passed api_version --- litellm/proxy/proxy_server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 19ac5c9611..d9642b8103 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2670,6 +2670,11 @@ async def chat_completion( except: data = json.loads(body_str) + # Azure OpenAI only: check if user passed api-version + query_params = dict(request.query_params) + if "api-version" in query_params: + data["api_version"] = query_params["api-version"] + # Include original request and headers in the data data["proxy_server_request"] = { "url": str(request.url), From d67c63b0c3f13215b91e5c0fa9719b1e97ac8427 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 13:33:58 -0800 Subject: [PATCH 05/33] (fix) use azure api_version --- litellm/llms/azure.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/litellm/llms/azure.py b/litellm/llms/azure.py index 01b54987b2..6a0c1ec1fb 100644 --- a/litellm/llms/azure.py +++ b/litellm/llms/azure.py @@ -333,10 +333,17 @@ class AzureChatCompletion(BaseLLM): azure_client_params["api_key"] = api_key elif azure_ad_token is not None: azure_client_params["azure_ad_token"] = azure_ad_token + + # setting Azure client if client is None: azure_client = AsyncAzureOpenAI(**azure_client_params) else: azure_client = client + if api_version is not None and isinstance( + azure_client._custom_query, dict + ): + # set api_version to version passed by user + azure_client._custom_query.setdefault("api-version", api_version) ## LOGGING logging_obj.pre_call( input=data["messages"], From 2f6e15655a093777b912e59f92ee78a3cfc6acc6 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 13:38:29 -0800 Subject: [PATCH 06/33] (feat) set api_version for Azure --- litellm/llms/azure.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/litellm/llms/azure.py b/litellm/llms/azure.py index 6a0c1ec1fb..9f5b04a041 100644 --- a/litellm/llms/azure.py +++ b/litellm/llms/azure.py @@ -270,6 +270,14 @@ class AzureChatCompletion(BaseLLM): azure_client = AzureOpenAI(**azure_client_params) else: azure_client = client + if api_version is not None and isinstance( + azure_client._custom_query, dict + ): + # set api_version to version passed by user + azure_client._custom_query.setdefault( + "api-version", api_version + ) + response = azure_client.chat.completions.create(**data, timeout=timeout) # type: ignore stringified_response = response.model_dump() ## LOGGING @@ -408,6 +416,10 @@ class AzureChatCompletion(BaseLLM): azure_client = AzureOpenAI(**azure_client_params) else: azure_client = client + azure_client = client + if api_version is not None and isinstance(azure_client._custom_query, dict): + # set api_version to version passed by user + azure_client._custom_query.setdefault("api-version", api_version) ## LOGGING logging_obj.pre_call( input=data["messages"], @@ -461,6 +473,11 @@ class AzureChatCompletion(BaseLLM): azure_client = AsyncAzureOpenAI(**azure_client_params) else: azure_client = client + if api_version is not None and isinstance( + azure_client._custom_query, dict + ): + # set api_version to version passed by user + azure_client._custom_query.setdefault("api-version", api_version) ## LOGGING logging_obj.pre_call( input=data["messages"], From 65ccfc35ca5b5de0386b4fd6b432d1c351b7d4e5 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 13:44:39 -0800 Subject: [PATCH 07/33] (fix) azure extra setting client --- litellm/llms/azure.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/llms/azure.py b/litellm/llms/azure.py index 9f5b04a041..0f67869027 100644 --- a/litellm/llms/azure.py +++ b/litellm/llms/azure.py @@ -415,7 +415,6 @@ class AzureChatCompletion(BaseLLM): if client is None: azure_client = AzureOpenAI(**azure_client_params) else: - azure_client = client azure_client = client if api_version is not None and isinstance(azure_client._custom_query, dict): # set api_version to version passed by user From 321769a74dbaa2c8d7dd94688a64a568f235c883 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 13:50:34 -0800 Subject: [PATCH 08/33] build(model_prices_and_context_window.json): add pricing for whisper endpoints (openai + azure) --- litellm/model_prices_and_context_window_backup.json | 12 ++++++++++++ model_prices_and_context_window.json | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index d56450b234..18c4b0d9a0 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -293,6 +293,18 @@ "output_cost_per_pixel": 0.0, "litellm_provider": "openai" }, + "whisper-1": { + "mode": "audio_transcription", + "input_cost_per_second": 0, + "output_cost_per_second": 0.0001, + "litellm_provider": "openai" + }, + "azure/whisper-1": { + "mode": "audio_transcription", + "input_cost_per_second": 0, + "output_cost_per_second": 0.0001, + "litellm_provider": "azure" + }, "azure/gpt-4-0125-preview": { "max_tokens": 128000, "max_input_tokens": 128000, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index d56450b234..18c4b0d9a0 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -293,6 +293,18 @@ "output_cost_per_pixel": 0.0, "litellm_provider": "openai" }, + "whisper-1": { + "mode": "audio_transcription", + "input_cost_per_second": 0, + "output_cost_per_second": 0.0001, + "litellm_provider": "openai" + }, + "azure/whisper-1": { + "mode": "audio_transcription", + "input_cost_per_second": 0, + "output_cost_per_second": 0.0001, + "litellm_provider": "azure" + }, "azure/gpt-4-0125-preview": { "max_tokens": 128000, "max_input_tokens": 128000, From 6fa585d001d6c1ed5cf3ee2812f407186554da32 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 14:02:34 -0800 Subject: [PATCH 09/33] test(test_whisper.py): fix test --- tests/test_whisper.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_whisper.py b/tests/test_whisper.py index 9d8f038c29..9090f42d66 100644 --- a/tests/test_whisper.py +++ b/tests/test_whisper.py @@ -8,7 +8,11 @@ import sys, os, dotenv from typing import Optional from dotenv import load_dotenv -audio_file = open("./gettysburg.wav", "rb") +pwd = os.getcwd() +print(pwd) + +file_path = os.path.join(pwd, "gettysburg.wav") +audio_file = open(file_path, "rb") load_dotenv() From 9274245a0b6f178d0d83fc14b9f618f332413a2c Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 14:10:19 -0800 Subject: [PATCH 10/33] test(test_whisper.py): fix getting path for audio file in test --- tests/test_whisper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_whisper.py b/tests/test_whisper.py index 9090f42d66..78e885ad33 100644 --- a/tests/test_whisper.py +++ b/tests/test_whisper.py @@ -8,7 +8,8 @@ import sys, os, dotenv from typing import Optional from dotenv import load_dotenv -pwd = os.getcwd() +# Get the current directory of the file being run +pwd = os.path.dirname(os.path.realpath(__file__)) print(pwd) file_path = os.path.join(pwd, "gettysburg.wav") From 2d71f54afbb2a29d9ceb7783524c7e39ad22e366 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 15:18:06 -0800 Subject: [PATCH 11/33] (docs) load test litellm --- docs/my-website/docs/load_test.md | 82 ++++++++++++++++++++++++++- docs/my-website/docs/proxy/deploy.md | 14 ----- docs/my-website/img/locust.png | Bin 0 -> 111279 bytes 3 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 docs/my-website/img/locust.png diff --git a/docs/my-website/docs/load_test.md b/docs/my-website/docs/load_test.md index f568b56961..94165fb7b2 100644 --- a/docs/my-website/docs/load_test.md +++ b/docs/my-website/docs/load_test.md @@ -1,5 +1,84 @@ +import Image from '@theme/IdealImage'; + # 🔥 Load Test LiteLLM +## Load Test LiteLLM Proxy - 1500+ req/s + +## 1500+ concurrent requests/s + +LiteLLM proxy has been load tested to handle 1500+ concurrent req/s + +```python +import time, asyncio +from openai import AsyncOpenAI, AsyncAzureOpenAI +import uuid +import traceback + +# base_url - litellm proxy endpoint +# api_key - litellm proxy api-key, is created proxy with auth +litellm_client = AsyncOpenAI(base_url="http://0.0.0.0:4000", api_key="sk-1234") + + +async def litellm_completion(): + # Your existing code for litellm_completion goes here + try: + response = await litellm_client.chat.completions.create( + model="azure-gpt-3.5", + messages=[{"role": "user", "content": f"This is a test: {uuid.uuid4()}"}], + ) + print(response) + return response + + except Exception as e: + # If there's an exception, log the error message + with open("error_log.txt", "a") as error_log: + error_log.write(f"Error during completion: {str(e)}\n") + pass + + +async def main(): + for i in range(1): + start = time.time() + n = 1500 # Number of concurrent tasks + tasks = [litellm_completion() for _ in range(n)] + + chat_completions = await asyncio.gather(*tasks) + + successful_completions = [c for c in chat_completions if c is not None] + + # Write errors to error_log.txt + with open("error_log.txt", "a") as error_log: + for completion in chat_completions: + if isinstance(completion, str): + error_log.write(completion + "\n") + + print(n, time.time() - start, len(successful_completions)) + time.sleep(10) + + +if __name__ == "__main__": + # Blank out contents of error_log.txt + open("error_log.txt", "w").close() + + asyncio.run(main()) + +``` + +### Throughput - 30% Increase +LiteLLM proxy + Load Balancer gives **30% increase** in throughput compared to Raw OpenAI API + + +### Latency Added - 0.00325 seconds +LiteLLM proxy adds **0.00325 seconds** latency as compared to using the Raw OpenAI API + + + +### Testing LiteLLM Proxy with Locust +- 1 LiteLLM container can handle ~140 requests/second with 0.4 failures + + + +## Load Test LiteLLM SDK vs OpenAI Here is a script to load test LiteLLM vs OpenAI ```python @@ -84,4 +163,5 @@ async def loadtest_fn(): # Run the event loop to execute the async function asyncio.run(loadtest_fn()) -``` \ No newline at end of file +``` + diff --git a/docs/my-website/docs/proxy/deploy.md b/docs/my-website/docs/proxy/deploy.md index 6de8625d03..4b51f094cb 100644 --- a/docs/my-website/docs/proxy/deploy.md +++ b/docs/my-website/docs/proxy/deploy.md @@ -350,17 +350,3 @@ Run the command `docker-compose up` or `docker compose up` as per your docker in Your LiteLLM container should be running now on the defined port e.g. `8000`. - - - -## LiteLLM Proxy Performance - -LiteLLM proxy has been load tested to handle 1500 req/s. - -### Throughput - 30% Increase -LiteLLM proxy + Load Balancer gives **30% increase** in throughput compared to Raw OpenAI API - - -### Latency Added - 0.00325 seconds -LiteLLM proxy adds **0.00325 seconds** latency as compared to using the Raw OpenAI API - diff --git a/docs/my-website/img/locust.png b/docs/my-website/img/locust.png new file mode 100644 index 0000000000000000000000000000000000000000..1bcedf1d04b7b4debe3824113e4ea724f9bd4f8b GIT binary patch literal 111279 zcmb5V1ymf{(l$&W3BeNxAvnRE;K4lvC%6;bCAe#V0Kwf!g1g%w3GO}&PH-I@2A{t< z_gwjI&cEJ!X04v?>FM3QckNwOPd!x~swgjsfl7difPjD@^-f#~0Rd$e0RfTz$z$LQ zkFKRH0s^|Qg_xM4l$aQqqJy2Og|!I+!n@D}bz}|Yz89I=vC+cj4<6=xT73A7?m^C{ zQN(vF0R^8Zg=twI2ZRx8s@O9XeD=f8#eKi$r=i$6{B8Q$P((x%56xv=4t1-k-0Q$= zY5sc2clc&=u+eb_pHq}jDJLZ?VFb)xKg82F^E%Ji+1Sv9 z33=fQiwk~>qh+%)wRbaceYwD%k#$5ranbbtf%OexucrvjsSMbg4}{~7O(#yomCy-C zu{$2V(qrt}cFwz8+rAUB#8Yeb;~|w^%xucXcxz!*JnZlhha0Vnjqt8SEuLfCEcC>U&G++r>0rt?^7~N)_KunrrozaE zk|aGs&kd12BL&SY8=wku%02B3ep&X^DVQvZ7_E}IGyJmg0V`^j+)CkCn1;DnYI2v# z;pQ7dO&yC8M_Ljw5VMZ88KAp? zmhw@I_huf&K+JLZ=oV?svB%@0z2RlhF!K9M{TeSu`DZ4Q^fw=seO#jBxr3&8xP{)b zzK9wLL{ln@$`3szH(GaJd6beK{mxzR0`=)*jUS|={txR^UgH`gib(1kkv7M|9wK5s z4Rrc}e2!{rE*?-K`6P-p8{r1!8R;5k)Jfdgv54SW8)o(KcY$|tXBz`=js5eVZu%)k zW0X@>y>DMOKl+7Y{1EYT6RlWcN847x;4F@`%f(&xL*4~Ar8 zk0CuR9~Jn~NxTgl+wmK&NBLW?aWb)aQKFteP;@`dDL)Ka$*Lm4YRdM>{`R6!XbQ3Z zQz~-<>HZ_|SGKaiT_n$W{BDC4!Pix2#+qTxxvF2`Ik)xrH}>4meT*g9q)9~1skGtz zN?S?JxewXJ~51mRLS+&nr z8Y%HNH)Zy!K@@AuH3c;lB@d(A#9Y<1`gD4Dd-Qr(j$8Tf(6{*rD)Pci!yawpyE{?e zkRNVazk(P%U+5d_!$;VLN!(p|;80!LlJP>98iRZheu3HEym4FO!y8BZA;^7kL3%f= z$^I)}4ByW&q!IgLYkEg6GNPAE!A}}Ftd|eiKVWRGKGgB2Ieheh?4h`yM)VWvjy?8Y zym*1zAGkCS!bIpa9!&X3S3I+R!1u$pf|MiR`!C8jNX!8Pzjzza)D^hh9$fw8JH!`x ziY+Q({+Yz^iw2p?XPkE7<`@J@^w781G0!Q9pOIr5b5%X1ljx6@HfC1}-xI^6pouwo zN8>=~{`r)=JVt>wxF}nz;FV=)iL=Gb4F)R=L77}9LXvQ_ZaebL`o!u$u*HR z;gh0GqC3>yib`s1Y7R=MD$+{W^tkj13{Vx`GAD`clyS2v-kE?skw~(WUiQ9$KJC7t zHJLTrW3#ZwU12K{p}EFNFBIu%&*CTIJ>$9i`}_0y_xs=XBd%ZenEg7u#`H>rG9@>Fi=oCsXU21iB-g*imX~kui%_%nP3^X zg9}cwH0DI(%$qf?MXYtGMY2qqyqfIWN!(#{j=a)2qd)7|;RYjfm=hRrkaIY2BwFrG zz9}F3E;U{=i9Wh!@xsD(j6e4>Ie2W)oRJ`@F1jwkC3UJ)VzO%7(8>gO1axX=bO$mu z=`s}w$^5FD>n#hCL7u3bG}txXWrWO>JBn>iJn48g`0VDH%-hMR2DX)!{dcmyO1A?ers&li)nL<7)7y>ijnyHuG)T%)C7JeX zMmEMNt@K(qI}4MP4~@N5V^uxUkT4t752|L)BdL~9BY~l&?yFRcv0#HZlj_BdDkjTa z!!!eJ6VE=(TE|+P>C*aLB8ivlg3@n_Oef_$aXs_ATfH-G#gK_ZUWbT=ybVeEJ|syK zp~rKzw`CW}961xIO?(hJMufwbXD@q_43V#s*U6{Pw=ya;Lh2bNoZ~o8sLo(tat}OV z!b=@+OOjwNW!BeS(CI2WD{Hacu%)vF+j8S;eR0Lr@Nhkf5C70?kl=0Eqm zS+_VyS#O-EHcJP;xoaq0CGkx0_}I+l>FhqyqTI~w?s}PgVee(>MRJ*XdF)kxDQGnN znmq-S%yLU~+vyW>vslfq;iy4#!wM&ZW5Xj6hWr}*`u&jo^Zd8{o&9e`m_@1sQ?rKzRR zq<&R&x`>|j<@k%>0~tj+w*>t3M%EK=HSwwQlAn8@_5^sOdFXgTf>PTV4dzt77qaKM zgzbrUeJisdnV6E@#S00mUng&jb4WX``4!~_>eM<8Uo&WR^-;YG**==U8XQf2l|Cjo z;j$5%GJ=YT=93mQvNCL$VZ-M6b1%d(*^z5RVk2+ZCc})Y+*}WKlYPneLh*$$(?!c@ zWkty$wEa`ikVLS=NjM1{`&34^(z3GQVuO4{f)H;t{82bx;uqa2+rD~iy}lr&p7G;x z7MVsFmK)v;NSHoyQZCc3#(tAm2kHvyau^|lx7t4K1T6!tXS3x^Q84%vOjW#8mQuCh z)#!NHT^nB;TdRv}fGf8<2r1Qo<+6Q0_DIkNX95O>3VZ-LnbM#61^VjqeYtx)B8v-! zYhjkCrtxccRh}2xN140CeC^fx%-e_~Z7A2RP3~N)EuQVc?6a2drBkJKF8M!)*PYEX za>`uQkIM!?x31jaIf&ij_v(BnmT88WYcKu&R>EsBSUt%Hq5>iFj}=!>p^8#Sj{s1iXoznOf7Nu~|Yrt|)md(=q?%WGy1J*SrG(fx6sE!U(aEawIn z4jl*3q|?l;gX3ny@T$63S$gx1@7W;dd2KkPquh1zGw9>4>G4pRcC)rXeYIU%Q=ISS z$-*V<2~2vwc!|aL!(sLYR+`YcQ=<#9=N7!;_Wj^@P*h`-K7XJ0aV}_!?#$wnebua1 zKwd!76UmcBDC?s6I%unFvvoy?^RC3l;`aELeyeXSOkwN5hsuZZ!eg7Tg?ny^x6Ow5 zGm+aN^X9@PjEeMBaPOk`!tGqSJ>M)V-}jYw;f3W%?WOisy=Nd#U?6#9GQ~CSj}KxK z;|Ojo#0Zw;2pkPU-(}+u;KsqduoAY1cQ}a$&rCK@g#~VWqjmy)xw#)qgK%xzvRk#- z5Mopi3>O~oy6o8#&KCEbCn!<0d;iFrE-&OM{I!(qHF7DoD3s-|0>;1by2A_i@Dkzw zo(u#Q2w^4~Ql@fp2=u`56NHBk2oN3tM-PBa@B!h!j^93@ML_!FJR$-@hy}vKKd+Go z_V=G?V7qVgk3CXsFak303me$nvJwAzHOg!@(m#(8@1KVtqAVsQ1?-iL9864X9nI~W zc#KKj04GrF-)TA`AiSWu-yTRQQ62*Khghg+IBCeq@*3IMFdG=#8JaM=+1TIT2Z7&> z7dW&raWWutv$3{yzmpEAokZZ^(l8MQXzh-_-E{IA- zM#k@8Y|5)7{`Sx2z%K!Eb0;TzUKSQtS660Nc4j*VGZt1J9v+t0Y%FYSOu!XPj_$Tj z25wBYjuij6$v^KSZsKUqkwKxV)*1UWf5`2V>6w}1Zq#Q$ii@$Z&w zoNR3W+4MjD`tPQyjwTLbb~eB>odo|qUw=0K&p-cc$j@>=^#2gWKji$!SwPT&sQfJd znlwRFdV=@BTq6)!h%2Z7dtjB_e;(ihU$p<&-yd&+p03Y(BOnMPNQsN6xINfgKu(@i znd?51)bLo)Ofv}A zFaf9M;*F$#OB~UtPixTVXafJ%Ld@X?)MOhP>LB#D#PJH3%p6Zi{;}BKne2CVfaJ&L zaMpc(7l;3IRPj1y+lZ%{tmWb0AGXBQNCAUCdsa#Lf zBt#4+et8CI8H_`H95Qp5zURt3%kK;A*HhYs>Lx1> zjh-^fQl$lEDIJ4@^zrBnZ-1?YvWc4|sGQ(m_4Mh*2(UD+M+7}=iGI_RyIsVus53K> zh4^*hk)bg}jSkcKm>2x68zp;7_4>@|LZlTVGtq6r5BgFm(yIGveU2}rc)Sjyb(-CN zg7<4zywrF{Ljz19z+P~u}ouj=z-#_D}WVE@zJzi3(0Z2Bb`rX%W zL3_$H7w#Kt+t24_4}|BxYVdfheLhG(=i6A?-;It`L&aqxfA;KImChEHB zss*gsuV=;_zpKZu*r!jNdxu-MCsF4XCYF5~t89s{YcjL&SoGp=sejeE-%_WXU#jqU zZpaF^h42Ycs}`xFnY(i7G}w!%G3rrIQ2Ww-tgIZ?Z9Qv(=uvPlO({8S46a*MtKVJi zvS>D1H+pfprxMM#E!koYm=%b*oJ-_Qaa2i2KTopt_L$C>J9O-5Iy;`-O2Xd5(+>&B zy2(;0R8wtD=aBG8Ig2abZ6PL!X4siZw9Au?BQ+a5uFbDcVAQPYjs8d>+iMGIKE0Vf zb@BWQE0ZJU-|T)O9YFx5WZwVG$PQm*s(lgQQb;xA{ukQBLH=~6sUhA%m(5!J#OzwX zovTiug1CmyeJ7r1p75pKKs=r3F&;fFKD%iR=1ijvsQK)euq@1^sRMI1@TM_&<4>sQBj(Ywgc)aXZKy5DXjL>*>7Q>-a*P*NMb&EJZ7CQ zu-JOrX~u)LOWS5|?Ac3wtXW+j6DIdxl_9dxr)Za#*E1*|D@lJ_xKA%2_r~?|S!d_- zCnxwXNq)bgf#YFWy1lLj3kPS>?2HIs>2ugHgPq5sLid+vO1ni2Yh}p$G3fYg8mfIY zyeW>AfiAv-a9u&41Efu6E8U&~mC~wtxVdLvJY6-TewE6iC)7s2Expk}D(e{%Wq8W* zWE*+vLZMS*)v;o}B&B2|QI+{m(&J^mgE^4s)#VPSy3|l|Z*=6g&@|_S=>>O?(;R*c z;k=b!N*#pQ2YtKm&bOj>%e*g@GaN;d#5srz&VUQu_snUTEON}vNpyXa`O1j=zm3EX zUjTLKCTDSrfLe{cN{4aKv!gd%*`i#I}WG!f93XV2K&_MU`tf?r5t5G*VF;QFmwWYN@r z%vL4fqu5_}{AT-pP`y%YGII_sO6J~!5bPJZNTJMv?d)4>EL=fb9MvsZd;3kW&d1Px z^&&FjtZi#zz6wdw(C}N&D-Dbhi_f;jC`T<@cq@*X1&U+j$3MPfh25MbePNyG$>j8J zQDiQBKQ!FBXs=d_CNauPuV16%DCSM4)||XHoT2Tc^$W?=V_E10WBd>y*G&ELb&2LC zYG0O6TZv}N+d8|&9V@ICHN~zSwb&U#ZxflcEZfu4yl?di7&E zOIkA~dBT`==Ifmv5J+4y;-f*Q9(?1GGJ&ctouwNd#~~|N(xyQ|k!p-&)*dZ;+m*bj z*KJ05l)SUfoXWcr4|mSc(9oEP{E_4%mw~X{0h6IH)mER9zi@c)U;tLUF2?!m2@p=j z`&^JL$$X$mp;~rwIQwRX-_U(}I3Ye;w_4jav70HiuZh!Y{fxD|p^UCl1Vo@d8GjDn z{qQI;lM@#@oFQNpVN{>hX0NA5B>2{)8|RZl#+yz;z(YO;+iQi$M`1x`Ez z18Fm-$P1R6iTXJbg?!r9B0XIz^o7C+eE1uqL@)e(DCh9(u1o{&RDa*OfJsWSOSA#>8sU|zr*2AVt^{XQ6Vw-5zy`Rxzv}! z?E1YCsw$gZu9*tkz;Y$s>M5q&qanExdM{<6400>=QouD;zw4sM%{|k#DH;8BTDa5; zTN*ypZ418Qn0LLq;cGU)�ex$cH^=p?6($O}b>Hb~GD_q05pzC;rZGrHAEZ zP>{|F-btSz;cjg#Oqs%96WLaj{-t#%D9-IPj%vT)lINrI5tLnfL5bIn`bv zh{E{Yy_3}f@l23rDqsNp@qah?a3TV4%wixE(iXLZGfiY)G*(ZYU4m0>ZB{6xr(5Y9Z$Cs?=Oo z{bXc5!m7+*?z;9aeHuK|%)A!m-ZpS*v2^8~+Ox!p6s?8o_{C(T|zLvx-TEI?ie?=(kL* zAAjfxS-&`1Wy{tkUAmquNhqA2u1W!zbk{LU_hE^pA^C;O*p3jazF%h7e6E@k6E6E> z5~gXW?pj}m_4dNNvScsby%@e8vaJ(LtFyLMoj>7CDyAU(Jrodrd2hX+sfSRTi7SN=Hc3;5PM=-nK_#ntj$3fpb5Z-DNw zAdlmEY|~kJB0Xo3^pO&k_k_!=cO;1bIhv2|hoDQqrWkRDtg*K|hGMs;Tu-pDt*=V8 z+PWWaB(zJQ$>><8@3Fp;gy$MThLu_kgZ$_WmHEu;4Hi7yZBAwGl4#CP%Vpp?)}mhz z9`8XA7JmT<%bp|c9_UngT%r~$oM>yO7MiB@`rlr!C&aMyk5TN~7ppl>uAdevPvzf6 zskzQmb8}geu|&S4(B`(OOIt6@)Xg}7 z&FqK!7pwLGfLBova-9I59gphp7e0Fjal7)HuJxXQW}Bt%kMDgk3?ZG*ds!~go>vs5 z*eq4aExH|X^0=>jDlR9knb@)Lbb)?^PDI4@L=Lnrc$y6D)S99W**v&zA*f2WFqVmD zrpuL1iVZVuk~2HBomrtezS}(rQ)@;8#{#-fg>0A`SD5)CvWv%gFKxrUi#QU^BNb;u z^D3l&Z;)LXboy28!OeNU zSO!UqWfG)^RZA!>=qK&$dncuA;Sos+9?jAmD*Q+Q(Ai1RaT&_ z&1QCIMz4?YB}2$p{&=mA@H~4#Ss;rQcUgJ>*-kFH3k`m%y8(dwin84_x(WV#zI~p8 zD7!GjB@q!dpX8$!FTQn#B|Qx6Isn%^r`73&laVD{ehAxo2tYX7Wpdr8^DY18ON&k(xEXu=M#D{z6YJ(E#C8zxn?GhdJ*4 zlbEx&Gj~Z9Df54ZM^d!r`FNw>eOc&KYCbcj!_vIimMI{dxs>KftmL9vH5Miju!%3E zfokd#n7r$z<*&Rc^<}>dCZCrM-k3J+@+vj4!V~l8h{?x#TvI`ciA22VTX$01TP5a6 zli9in1mLx08j@=HdaG+p&1%btz1hl%PebPeddj7TcRjN*5n7z<10%8$AkudAu`+nU zt8efsSYZ89Qdb)s=ba3#Ic(2wGv`B0`#0sDGJPD4+_CKjt4-#aR`=rSJ}n5W*#X?I zlxN68r`=j&+<)piZNHFOd%V$b^>Fw_P>6# zegNq}lO!Z0f@}rRAftuVBBRXC={#t^LEju*|IcCWYsZg16p0~F9RT3Y zc&jt&QXjNlCGMswVb@IfKaRksZgf~r>?8G|KH0q4A(->+$ozJ?NoqP!Gtzpvl<^I! zUfw8e*)5)D{-u*7T`kbXCEH<9p{;k5Q~|XiBXWfQ)jESJy5{J3%Rw`iA6dn^-Z zc%`qLx2f=$(-kJ8d1ReIb*@XVk|1SN`6KBuo4l@Q+s}JLo1XCg#x@BzJ+BCK^l+T3 zvr%}BZU&1A=OZ5&{DOzVG}|T~y0Re@IT%-P6c`~n%AAZ2{fi~c`@F%S-MP>~JuS~R zVY+ax!-biTKPfrPg#fe2EU#S$ zK85k2gCqP@}*$pkT#C-qQmobO64(IwPS|anS^cAG>gw|Tocdu@JjEdDhF`X zl$C%9NZ`S!JafO&Y_wk__8TzHYX@)ZId1e(l}Gq!x}^GUmxQae58x2fm4ESi?~;b< za~G~lX9wWV-YN+h%2^fFJ@%3|&fDFPgXV)B5`1E&1?1CsWg3P4QN2Jf+`~P29=QUi zQ8cmAewP-|X?$Ey)EwyIFDNG^%53lk2YSAYr~xSFA#|^m*VZu5eHLQ%%6v0dl<>D@ zNfs^tNXhDI`ky!{FByRP;dS-zI;bOzuM=(9T&8LNkg+-t!?^Di_LTk_J7^o`PMD+~ zxIEd^+3dBY_pfa%j+ksJs!QhvcLbl2Fjj_OHeP))q~Oep^7e#MySpv~E9#VOzP#6* z54VySTyRx~oIl}zR+vA5A}GJZd#a{#mL*pNv`er+TfaE=_8hvvuo~P#mamX8ZXm(X z>I{X5kyCsxf#y7hI&Ab)wFIBD8w^_KiLT@Wn~oLHZOCI3yT%i)#@6MCD!UOq{4u-F zcKX+m#3i0-e4dfAm&R4)y6UG3p0@Aw`(h|U&=1%(&JX5jFj6`0?LRJ`+a~W1Yjk{A zGkD3$R}*JzZxTw!<8k`=^k8-lu2}awspkJ2g_6tKEMyZKb(p6|rohgSIXzp~Fn%lW zH+S?Thr-`py%r5tQ+Y)ji(LaM)HG_mtW$GX=|sUFZ7EL|a-D*4C=&qc&w=3FA%{`I z8buTumoewszo_d;*{q6f$k_F0MZ(?gX0FP?qJ2X1g84NHL*aYyeyRm~ zti6QPL27JvhM znpH@ivd4G>HnnBEg`Bd6QU&+?>~eh600?6 zyx*S94>9hI7EvqLpaVh+R@`lQslM^t9ES*cwL`nUb_uqlQdo&m=KhQ-Djo~Hx2!}{ zb)H8iT&eBn*?8UpG%s~QLi2Edf5u=q*C>fj{Vlz|o^Dm@_Q%^mzdNIfh~EWIsr~~$ z5>0J}nNZXzbXhE)tI$13JZq=)JiX$+xGZmS<9yqw-iq zijA@J@V1bW9mgrM4N^~+{n!z~3zeNE`%)cu=M3XVP^S`Ua8d69)&c{M<3?3TouE3S zrfCbvQ%-^pp4*Mfxn2jFlir_q*8!d0;SJx)<)zZu)*kEa^Sc}WD0o$6d5P-2nZT{< z4aF$UgycxDTTC~{poE)NrIjjVOu0;PPnev4A=QCe|KQa76+pI{u*FTx~{Y=gpHB$9W9^)@MPbzRrm-J*@#QwG)N zI^_SJTq(T!Y=XGzVA?5CAkmRN=FFp4m+Glv#C4!}Q5QJs8yboK4)Xwt^+~uNalypT zjIX?)g_%5W%gF*}_mex_Bj1!hSZB~19N|&9qI{v0D- z{;jB*RQbZ^nd#nOXnr5ra_NHhyNJ{?~@6};_p6c_x=Cd&Xx$B5d@ z=qxPZ_y|O*^o)VW&Ehb*=CCziBvEERHg#cKlz_wL z=@UFNckZ!qF#7(utoYL43U&;|({G%e{&yPZU6XJ@yUVA&SX7ridluP5_7(;h6$vZ~q~Ge|WI4bbbt#aT3>3Tu^Af+nDA8f4uNv(D7SckhWnf3eTOB(s~XvfXr6l2fB!U|@+ zW?H+po2C-Y##u*nf{A-tvF3=V0rk(WyjwzihI{hvXo@P9BZ2V&NM<$TouxA$qDxY@ z<2wCc_)>VPAG`IY?*T!>T$6K}uj9LVK(6uZ_7(SjWMQ`qW&Q}x1n6f{Z!az6PB~wp zFPL*z$7VPJJfPlp;OOhf733tq^e{?^-Mrp-nViS{*x1h?7#*Kd``lV-lJdhziiKHA zBEUx_Hdb%CLBBzbDMTBD9iZGCZ7hZ=Jmi+W-!0Ug|HTphSACr{-`KY|u>{BxH zB(6*(b^qTC;eYGUC#_#syXCdP>Fk>jaSSN>92K>3e&B2mWnCL2=xNP~l2lin;v*=V zaWCy9+D4M71geb4NxuD^Hs&5s@pgNr9^jfZ{_8#9)&^^z zSCr9o>%?Uq7Zkq0#UrF4gxJQf_C!*KZvL!=yAaBT3|6C0&*}73q!XE_f>3c|ze`5;E&@Eu z<%)^Ja=ZKPMQ;IZBV&h2YbCQ@gZVK2?%Wh^OC92d<|WHDjQ9yJm2FMoei9vdyL0bX zu}X*iJ$&^v_}jORvi_ovm_kG)!ru)O@vIfN;{jfd@@fsI3sOi9dgdL>0>nEGZmRV5b=g6anshkp$Q%8;m%Fi?mnD#jg?P$^R(C7W z`k1X-P1$Tlwwb)%0-r|c-~+vOBP6LGZcqreR`~|4Sr8E z@6;A)LsIEvOfN0RlcXB?i|y_ZALT%zTgz5mQk5zIgR*UTvNQ_wIh=kL+tD02y#8lE zzif@Ffz<=kREXtfE8l{B`+OPKL%9`|?7oIY%xf(AFk`#13l>{SC0F?pZ5p>N*V699)K{S51lw5X_qgc4GH&)3 zsFa-M`K~GP^lmP`xA5;Mvs5d@p`j?~I8-YY6|N`}qh;@o43B>pVFBRf=18wRn3KBs zC=*3w4{HlAfkn#$3u;$u6)g^bgNLy-^GYD2&Se}q>y4}S?xk$QW-BJvyCWsvBuDhE z4+I=H`40;8eJ?bpN_3#$E=!qk5fC*ucU)en9cIIZS=JT-hf~ha6i?D71yno+0{irZ z3dlpfJg?(MYn}Q<+b~%5&N_H~9}_0Sg;R8_kC|erqbzteSVep_{;n^R2~-}gPOBKG zNg*ro8lJ0=l-YWi2ye|q@|5v!57HSb|3W~MCQC~_t+XHuaqqc>A_mC0_*MhMF4n|c&j>8#+169&`GCvTZiktkM%@Aw^rpYYa* zuCoAnHAPvNPVQSy_lwY;^p1fq7EAquc6kG}7U=Y?v;!8MK7 zE`4#E2iy6Pq(ao*pi_r4xxrxhq1!FxC{WW*OC&z;IC!IW>>HFxsZvP-QYPdJSN6HP zlT=T0q##<_T*;M5UHW`Y)qUghSR1f-)I!6_R1ArXC%2f$yT#Dj9UZ#*w1c$j@ho*X zK<{z1t!G^YSnGK^r7pNY51gS_S0^m%o0>ucLpEA{wLnTRMYdqVm&fZOS~lydlu_$Y zG5}KYb=Z@4o<`M51xDI^#kX3oztI;TMI$vZXO}=o$Z3D}2SzD%$I|?#5C1EV@aH*Z zUH>%zfP2)d)W`&Acwo$e*d& z9sC%VkfX5=b%SbS#+=rAs-E4r2}Q1}p2K#r^UDnR;s;0&8s1tho?aqm2I(y%(1jrT zeKkXQ8P1Z;<$2*|CpUHP&N_pa%^idDuD(&w`AtXm-R+*uncGS3QI*e`7D!6zs@3Y( zPG9B-;*<Z+!E2#7Nh_R2a9jH3uJ`Y&Uvv`I??ZvYk;VOajRWICZ)V5zm zIi;!qck6_=dxp!V3T3?uM{iQv6U)V;Q-$k`5ITG_Ajn~7O{%f(C&ut{PGf# zId7MF0Pa4$sCiif>QTu!#`&hTM0f471c5|aBGRUccy^n6x&%UvoezH$mu@yvuFOeH7kTV zNO>%ngf@AQ1tVvfzUwk&3&Dx}5KaLHug%k0*pVk`FX@Rd48W{h<=Gj$7C~*D%q*!e z=VEueTf9=+i}}&kSUy)0xbBhd!CbX^H-_&|?u3Gc83yxwgTmtkhKK1CjUs#kkm!#i z$SOw@6$@2DNxkICm2$=Sv&vDkki^Lmd#|ioUhTkcj5rUvysLHN6f1hlD;9$JT_71u zr&{*~UiPrwISlfVqPw4&9a;uo!L6AyYn8lHWx9>3u6N$UdmMG@4cl|M9ZX?J;^!{O zDjZH3njv!1WuUuKAE7YEiw`IMZ;Qnq{Wt=M(8d;Oc3IfD1VcsgS_G;UOi{h zF0ErqtJj!#FiV`dGovbfbq1-J%%WCw`k~U)kCW0<Q~Uk4}X1*OXO3u;jclh=k8YWr|Us>SM?%W!?uLt1DUfX8zZBA6?Ju z%E<*@Y8K~7vV$^eB?Y1>+$0zGHDhHX{o}I zpr&u~Etf;_7E@WT2d_bV$ysIenC(f*<8oSX!j@|aWmvnD zBz3CY?k=3jAN011*X47OS!{M6gyq+4a@1a>>l5B8{Wt>p4T=*~z*yLZsrvd#Co-?2 z!e(!M?hNW=ulIm@1{l8nv?4r`*I}#TOnV!ydyerzE#Q`TWv_$x{{$L-!m}^#4ML`Z z2ioky#7`CV;@!;1lg>~a)7c?cliO5hW^btLCK41;)Uev@j!fTI5DZr5Cg5n10;qea zW!5FwmHORjMzI{XljZ0#ON2wM)rFxk{A(5DQ(L+W)T%5NMA|$uL%9nni#sw+Rfbb> z2D%6hr&IMrk-*v>;-C(aX;z!X8B)7xlmYqFZDP)f)A8Pq4tMjKkV@$65(9z!N9I{r z1fG5zP7j6BK@~%sXeVY%AMgn2D$jIOzC5?*hu(t;5$4x0&dcvo_X(S&Gu1LZRe&Fh@q4;) zC>Ur0Z}o2V;obPGE(vOv@=y@-z+i*UVANdV7)6Xww%ka5acwzzM57yi9_j{k%?fi?DvkU!E$59S@Ia+ zysZ!B^TgbtxHLST3DL^pCElAWZ>0iw_RCpzAYdxBwPS)TIejl5(i@{oyv?5ns%ZF} zuWFjDizqC6C-qQqnHe)Y_;D2t?9cjDkEg;{p|(@`k6&J=9#;4U@nVFvp6txE?sz<- zemz&<{@u^MSm31ypLT%4U+@b5FDCglj$0AvQuq;hPS(e`m~J{Qef@{OuLI)|vF{%* zAsjis+r1uF9xeOuHGsNvk}sQW^3PHV55chDAp)uJIopp=KHueL^u0xe$TVrIVJf3{MVsTiyG=6;XP2=50 zo2hW|7EZc1kh3d-9aqRVZy%;%*Ln9~Dt%XzgVb7lIAh1@txd8jD`%-+@=R*6@ZLGN z25LzL#wa9ppy$ECUJR;*)^=K11TVm&1&$~$YS7NprfN(am4}WFb_&klAY&0le;vW7 z;VvE7FeTV~YszcA=`j2$F$9KBUKxVLmsWb_$;VHoKIt5Sowixn)PLiyHoXj|K*dKW1Y5`KqN#_yX1=LS=HhU-8hwHnPLhF<1l+- z-{qS}I7Pe%mOzq@!^YE9TX`F5sqR@pM={|rDqq~Y&5<%(WPL7|z^JU1xpVmi7_Mb) z`Ogu|!@Y9i;4hwn*@%-*ain>S+v%F(nbo^LeQwg&Js08-XV2M0;4#cgIn^Rqnpa7! zwOyV~NCnEeM$MzU8v!8xt_C{^@@*pKd~0c-R)#^_Lh)UoW|MfDrLK1{M#WSHeDqq& zx?8W!vebI9LTWgf&LXY~h&ds4YhCjuLl_f^(ay`W0Zp4)C4ptwP z?+7ON!tcEkzxla--aT#JJ`O~%BB*Smlj!f<)K$*sJ^6+fGongf3h4&@1fI0vm> z>oj_By62o2mC1~68zP@=?Rr8o_y$sULNQXgoVfay14GtNq6ANBEpio0U+(DKbhr(B zH|f=h@@xZ{H1%p9NT&C+$i4PM|>^}CY+SM4%c|BkYBXt&i3s;nj z5=WZ_zh8-t{qp6k_15#0EGZU*flRnXDzR{Zg}z!oz}W&$6z6G?PMurm-a=!Kv)p;N z2|n{lI)TGz^{p4Uv?g{|kzKCRmy^d6{dTfY6PA21VycX5-xZ6imLO0roO6<#(|UlF z=BJ3;<`?bAuPapH+h)Dc(1p5K$~JOmjosmUw>mQifRxg*H|?@uBI_|rI{?}5&z6b| zkr0C}0+hUiv0f_=Hsuq8g|{0g7Qc4UvM*HnR_u=c3^XNJdeC~}_6W++3of^^8z~BI zMN)2vT~J9V(P;dp!m;F!s>(p^A+71{+8yQI^^gi)XuWy1AhQj-%fVU1`~jFzxx6Omue=Y>fe{%g3e*7hswI@#p=Q=g+D2XUz?TbnC|rL=E&bR z#d6!_>=GT> zn7X?A3%5qQ(kZ3+dPiFXYeM>im>gIf-i66(`sH)<`|=2ktITUsJnQQk6r6K5@aVmW zU02qmwog>BKr%&M8!Q^a(us6b_r+ljKqU4MH3RP>P@;&wHPr)o9(hm=;b{4p=YOvI z4w0clZwkSZOTGM~>D9IkwLQVoxY>p9>NDuh3hC$x>MYaUE!O2PC``9!hENYo*An1D zp-+hO8hkLi7`T>=QutWgmV4>eYS7Nl5vV_y{6PHwf`|VKh5z_TmJYm7(_WbNxiSu? zW*5j7KWBPB&)FnjpFgqRRbOL0r$qp;5MUE9kPteTDF@BQ67ctD&_tyK97>G|%Xo!9 zZgJQgUfqcWUW@3s#(a_C0kP_f=c&e(xAyKS#eNpk2k!E2^?3C$o=#%4ajV+DugjPzGm9(e7jHMpoMkv?`mE7wQN1s3k6O2vL-` zOx}mk(-oRnukSq}+v7QE7e#4NWPJUrA6zUw=Xvm?K-~NQb~PD2BT1cJW74D8y^-regd(*JD-Z5(uX;vA> zJS%U0Y7Z1Xati!PSro|8kb*5y0dKZgh_=FIp}V*>*WUYf$J^JWkJ6P1v2kh!QW)w{ z5V6qSw3=<&I&7S7`?-L=`lk>yn9ONI<#yx#H#!5s{~v{UwuZrg=I#INAx+%jWO2n1 z^4{S-20)UI%^jiM*O*43TXJQ=%(b1=`0NZd`9CVYvlT@kFPlE@oYLKwb?r^piQN;m zo~{Q8J|iG8E(-y3vn3SCymH#+e)UnhfkJ;X~rlY1$prUHN%Q&-Sq zU)zHSU+qK|Jxg7~H?1Qup{wl467Qh!59foVYgWofuNw!V@g$`q_hxiM+V~wx00_|s zBsrGPbTbdH_^PbOu=OGK520121JoTu9HV#P>}y+X3iT~Tp4c@fvEMNM2gM}1SIn=e zpI8CKwDPf%0qX2ScNyDm*%YX~4?~NGe8rGHn)D|*aw-6oHiH=gJ>bOnUt_fxE5@-}iQXE`PZ|hSOZV%hwl7+INx(T@tpHK$M?T?t?zOz*PzVIecyZUYwvySz3&gnV2|3Y3L9V;E_TXkx&d6kl;I?fi2$`GHs*Zg3$`k#F#x=3$XV-39XRu~9s`fJ3wP$pZsJa%Id0- zDul?tQhP;e>U>Q~QLfrXpn0y}3e=sWAUIAnfPZ_Cgf>t8!b9XV=7ZgRYg)-d#3#}@+^#hc8Hj7K=3sY#)H z?aJjaBbrGryz5!)6@KmGvvZ*Nrx9oJ|Kzo9A$_zl}}`@z!{SW0n`x12`IP~ce;IHP@)}2C*%>fUu>U~48ULe?Jj`z z`3u?mZO1io^#xhl?#_?Z3XnsYqA}_p6&a83fsP&XUUOPUaOytW-Y8yJtL`!C_Z3)d z0VXW~^`KQLcpk<--anlE#q?6!&MT++VY&Ojgp-1Yq7BzgemXVEKAe%fp5aW{nbJsqz+0fU zv!0lz%AP7Nz~y>Ty%C#<6n1u>h`9fBuO^geZ%UBM@%vkVj4DSi@iOT;NX$UyXGA{UJ{Hg_O$e7Ss4TjjFXef7%Eo1#AG(&FR+Rki z9O>ElFKM>2v#AV^88LiqM*DVt{+)Xh=pOo}f-Xyus6?I3uJ;go#+SEvGOt2L@06b- z!_Bo6l_sts?+Q}xO0;1REohL-w%8p)oRJsg5_w4m`Olj-eT3wlB zrR4;jrpBNSfEQ{#%1j=08<9uUo++p*4uhf51!|Gv+ycuyFV)ygN6yzwmWDbSbKEch z4RNet8K?>1`B<>xbyGV50t;;?dimpZ<7^6sHS=@3Y}CxSSrqr({JIjU9=_y9N&KTVmcS@MD~5yo8h$b0l%mFN`Z~Vwox|b=bCSSDMHb%`!gh60CQgI@ zuAT0Gs0hQ2ibTjRRs$-s*5^F!U8Us&XD=>GBREAxqN{nm`?)!~lJ(S1Ns%92wfrA@XSVgpVt=9pd` zIo6ZS=y#IO7^NTM?XHah4!FB1>t|1>3*X021k-^<0Vi~k^+QpnO6U+l@ z-UtYcMWu&8GzFzGHe=f9NY+L~Ou)xi(RBxUCbHNX#`%m&{YP{j^LDZ)Pd5UCREuZ+ zoD&HnD>>WVWR1O--GH+^o-UxtxEs%=h3~!^ZlN{36W=^~-GGP@ltZEp@TB^kZILog z-@Y}3tIxF8seUU3S_rCN4jmgGd|wfJ7j~5HQxV$hh=H>0W!*Z|ltgI2h~G0{7Ij&0 z?`$&rC17@bw_jp@qBBM_TuC22pG>St*B>Iav+L1T#X7{toiHCtHxq1f>g~~HvHxzX zcCFx@QW23Xr?GLZ#pcM{S_cD|n)9CR!-lmUJn@g8iYUfR88tgf0PXkYym=p(n8Z%S zVKCvSQDq*o*p(!mKb%W3%2BIWnxOFU+I~Q$X8gRPDNkZh6QEnDGjyIVc%M?RJ$gJ9 z#PaZp?O>khs-I~HsPBZpG&>^rfa;2(_Fmjh$~DVrORwQoqONxL`{0Ka%>h?w58#=D z?dz-^PYI}9_g~hAI#?s!n{bug&u?3Umd_#~$pcjzG3f{U_iM%$0Q4%q+@rPVxE;m^ zbp3jo6Fas{IF(urss;vyM!>q+AR1-W?_Rmd8~BIXzyIRNG~4tEIQYt z|5dpQcFlmY7c6G@a>l~N3K%SS!AR1-0ocLOt+P-o=D{Fi&QXoe5_50QN4>a*OXOTP z8_U^;(KcL_X?cxlXp1!(CY1Adsor<{ zDW#hPH*+{oJY#?5-br`)`8rPy0>>o-*9{d}pZ#vcVcl5{;3KF)8L&tnb~!d&ZSQm% z!IqNLW*l|WD+n#S_VIdPJw-f)+pm&y>t59Y?!QKOtp^z=)SPV>rcp8#!0kw{y|mU1vKe|-&H!8XMCRVc(V&tGxEo6 znx49|Q*T2F)1sTU)@;3-UIdzrl{np8E1=$s<`Kv>1DwtN8YyeT*iebk#!R?m<5!GM% z6lzy+n_kNp01aJV{4t5@3wv=nsYBD82l*KMAg4y4DiS&vdxT<<|E7eMK z2|nD<=srPl=s6l$&OAK3?yl^8&keyifSr9cygyl~tJOKtAq7y4B8*sbhpWML?y$i_ zQt^4i`KQ|*%jgLg&Zm|4;%HnnjbsXv$&LG|KMA;;dyZFliVGeCqsePSdEM6a8pRg2 zB25QF2zbk*soPUwvpwPCtlkHc3p4i*k2P}C8h{y%K#W$i$6G+(V7yfLwak-^24QBU zxhr_zXUUAWQZ84XaI2!^__2k+^M#wVF~ z`n^sdd7^|z??BG6f(yfhBvrpLd9%eEtSsD`)-@RwL%Ax0!&_5*)bfWlKw{kwG!fKS z#)X!{`JCPE2F=gUFWzRQUo9qp8h(XkXd8URD<2Haf<;HGX0xb&!V&8(Z`ra!|?LBL%mqiLT(9y^)N>4yd2n_8tAxks`4CN0H6BmM^vHaPH{khZNO0+gifI zqc{A|Yy#Hs{48F_w_tBII!cW)zZKDE%ADGRB3D*!@@sIMKu6-Z0L%<` zU8)0z&Xg0FrVxFn$H`VtO-z?Z(&JNoI)mEM#{UE}NL0$#`UiEK)5D2-78 z-6*oLoDz(mo#@=JQi4bpsR5%MO8oJO-kx@bLIg40qr*?qR<+@YypmKI)ccUkN(sRWW*)PxU6xqi5cYwC1=-s=^ zB&pPwo#0PMpJ&?kKUb}0NkP>l6maW8`%kAbT&oc|V| z2Ds-y3wA_58F4sW*5>Th3h1QQxWi(&{X^L6(S7jgmCbCe2q@m%q2qMRVyskf&UeHn zkiYCB_7fu@QCQZ1;}jX<(dn(!&3IdAw=^=}*#ky_O*g{$6HxY(xGZU|3D{JqFyn1E zeCp4aqT+(sxukO&do@)`1k(^loQqRWf!iQ>s-Ce9i(~$$z4h}<&Z3sdOuK}I%q5SI zH`qzW)Bo(a*=v6~m{AX$xSw?ptF_#~&Q$#Hp_9~N(bK9}^FSUxTE*-Gt!Mvpy!Wq; zn)t2YDP;(i8oH0P*4xx+@~d0#wD^Uf#7u1eBG|Y|t@MHBk6&i% zzs~}#)yFou0oRED?Pk;UEB} zBRtkl)8Fz~bCwc%bAaKC>T)EsM>JFw8^cc82RD$pyX{^5$C-@ZHuCrDMJWOV)aS;6 z|L<1wA2A7X1J`MRdkCqj{$?=muLz})it{gaR z;f)T{|EbvjOK1I>3=p1jyQfZJ{M2!n-egfDk^TQtrT!y{fjHpg9ghTS2>xC&e=j$m zzMDc^5oN~zm=OM7wuO?(3TW}#rjI?(PaQ|v&3$y5@-KhxJ~|WN%soutp4l4{s^Ne|Mg(MJLX&e z4H5qoY5ji)5u2_{jyZ1GKQOwwj-0mSptCG>T_6wltCPUd5ynE{(p8Yh#726DhA;Mh zfARa3FOukOB%wueG#w=D7hX9hzHP3n*n(KI?8#`@?0XT0A~+Jlg6$i|9nHpN3El$r zYcpH%$#KA+M`zxKP9bvgdfB%v zf2~=+E+OJZO$N-#{4a9Bwchn7h)*~yT@=%%ryP1>G;EUT1>`B@;^@@8kEF)?`{$C? zOhv3b&q{-{s;bniuh)wz+j|0Mxh-Qoj@El8pEsUwo%&qNRW&+bl=%OobiySNQQ$Zl z^!+C5aLRh68Rz9ET&Mz*4N}%s*>>Gk9=ipj(u3h7_TS!vmx3d3w_LuwzB*vtySA<& zT{&@|Io9D?tEa>}%mzC=|7p3H3j%UMPaSPG-0w>p7#Nt3_nH)_7nwOKc9tMX({^wu zqpO1#lwQ?1m9F|HOgRKw(N*u_KVf$(zt}k!2%o*C7&VyIAI#~z1J$ve?z6eJU{$_>+xF7g9j5K1kN9{j@|8|ANuz z^`)ZpbV<#A9_uH;QIMD#Kq~SqiZ?ytkJG06+XzWl60ar4>!IRzF|9j0^lfY)yk|`< zd%-CeU3`_-2*~YAquNH>E4cma7=*yJ2D|J$J@mRdWW5lbh2-A5{|=IWx7|!^z;?^= z1E~e{{XpApy^{9p=kiq!2T|%6dBb@=pW72JpUV)AS(y-5Ao-4&R9i;-7 z-)0B}))eBLPfg%(TE?q7NBESPO~SRu(vjG}&=A?|MXUDSK`CW-%cjUrc#E5*N!&;9 z0|n*!L0GguZ-{<-TN$LQot2ym~bfgNsTYSnnOH(kZw)B-(&3hM&0c!1q^>T6piT1p zz0-zKKf10{OA=DgdDk75%XZR5aM+cW*F^?$qTa|)`5g&*Ko9bYY89S{o8qA<9X;oI zQ*AP-)&b|7#aD0o4dE?R`Dm|QC$;y^EFd+z7`&^3T-O8gu$?)pz4lAYj$WeEJa@~3 zt8>`H7&V?G{v)6BE%5BcCU~o4Vb;-?yoYD>-<#R{VST#E6%9j~Ju`6bF=qP?V+pNk6z zqyUB1v|!L`z@B)&5LKfNppWLNU4UFTEB}EP&_96J3=C*59yjVd$Gq4<7@uCi8|T`c zIqRu{QJW-Eh!mNbCS~R1h+b2ON>F%ZpX4zPvZb zRad~xInNodFO6KsDLqHAYvLet4P3Ps1-k%Ez09cbG zecQ)i#xCsj^D(L7jgQFZZomJN@Q#2&76NQAa~S2+=jRZI_?D08jk)En6SMx5UAz^< zNA)p@2IbT9=r+f~y9?ZYb?tb|`MTb6#mdCK_ zuq}cB2t!Jhz)?s)mkfOfe2cc(+8akqno}|UNMLuO=ID&)e98(kO6R3hNv}hsWX+^x z1$=hCZ0rHa&%vq;_G4y~I+6|Uk}>nWS8$5yXvrsW7pQbcObEc`&jCLizo z6!r)blOp+wf;7uLgn9TXE$3!w;-xrJWMz4zNK&%faAW|LV8Rprxs@WIEMmRbnxtIz z3o%Qgb7)IqfN@X24Wv2v=Yz|J_XMjzbCC_7Kl8j71>w+=O6>t^Ja|ef99R=7 zcW48Je=c;fXD_v;W9$EfiGabo%_b8Vw#FzzIFq=cwhW&8o^74OxG96t||v z!dGPRHl*%mKVmIE4Q;oP&`{xCR~v2c57!5wxKp@2UYj1yuQ@eeR-GIL!yT=*_QaUq zdm5j~CzgH1OG#QqAubONC8rkpxeYy`U^2V9|Aeckn|@9#pf^&ZC41r=P}zeQcopJ2 z4(6KKp-u%^G*1CD@QoJzIpy*x#AbRAGSH-irIRw01)HJ4ef?~CMjd1PGKX;vWqF+v zx1Uwy!LobU8*KeR{LemZ16z1TjrzaJBtrTZ99Y&Ya$4!c7`M7qF$jdovG2*n{{>U` zu=|{o;`YOi2>5ruD=pU93~BnKLh=Br_0BLDNbDu)R+gu1rPm%jHdh_7O9ySs5l z%=D(mC60Hc&-Z=E8wCmmo;&xNh5quz%>S`!b!xKmf}zR8Aovh#&8c;j(-KId(S_g= znX3ZZ69D7YxQOTf2;n~INX;k_;VlnZ|-&;z1HK&wT&ZSC(px{FPOL~zT^$rH>-x7qU{&uV}>?HgWwIDu#;zw+jfr0W;+q9)(LyxwP6 z+gOX%+32M$FYs?-$ojw%Y2^os%acMw{3jdC1TOpc;0Bb$aCs+>1%|^v4Zu^%2`U6+ zmN&e(%$i%D#JT0j*1($e-Ktqe$Y1)|O)xvP=vl z2P{%Jd@)>K8^h-f+-ioEvjeak>w)f;04W2dcTgfw1~g;x===AtoRbx1$TlGu)PD}Y z1vODOuRl?Xclmmr4R&gJUE5)jz@pDusf~iC-VXmuPTvVH_dck$Y53 zT+0}2_$&PB=B4kMs&!!?z9IsF{uGA!Jiv+dYN30=YLJ=W;0Iy|lCdT(hZ$D9qWX*8 z{Ma^e2a>O!yu-2FQcYpTG^f*M{I_0M<+qJUt!H~NwutX?(7`tbz4S`Vw$_DjeC_IJ zIH}aYD#tpe?%?R4^Q%(ZX%EaK00eLO`sdg={1qbQ)rp~Q54i4Fah9KF{5cTFygrmP z`_qE|pdUUDuuyRLmtY*enI48HjPILJXT1Y#G)FyFfH7Mcn~>qsNJj1Ezo_(AF}-$0 zQp2MV%9F07nmLi9tM_<`8~02op8`I>D1(st&Og~&I18dalH_YOYjDNA6d8?>?ofFZY$ zC@}b33b2S}Yk2ph54P$Yj?luj{XiJemPObTMg{!T=>NEm=ZH#=o{?H9Y2J328XKbW z)~=QT@m0|8yzs$75ovmMvbHus52(FIPJXIG!j;$yu@Wx6Qv4x~X}!zK-9p`y&9Xo6 zdJ67{XBQlAG!s6SU)4zu5lI3V`TA}pi2^<1nc`sV6VmBLY~Uha7N}1jLz#;7mwyxn z8Ln-1>>7M6lK&hs+P(EXhC(`Oh&Um63TP2mU-vIV@Ogz2`<#bQ4cQA&>Sx;|_^Pt< zB_Lh@us9z>ER-{e=C1J@Xg#T6X5cZN84nPwKuJ7V zi1d_XbluL_yP+vsc1%}9BI5}DFAxcQOYQp#VdyBQvVHTZ{LaJMiV>K|JS3>GaB2C1 zXxqVqJ%s`fKT&bEAKI^=hSdCF_Q3}?Asy&WN9p)iqnyqA_Ro=|$n+W$$w?{}g9*~7QrMc9Wqw+%i{&$Jw}p_kUjI5E{p6iU%tgze6(Rum-@s{i3& z&E<(DZgFNil_r(QCRunq@z|O}j{(oW)Kwo#(vQ^4vXUxbbmQkybm`rH@&*}Eg;=k_ znQQw1aR0Y>ge0GrODh0(> zC<$tAT!?;&XZx`l!AB_1j^VhT->O%4Tv;r}lzA)xR0~wI935g2jn}+;J|8f>HZipn ztl8f4D-w9_j{67>pu}#!K!Hc7iVSyP3Paaz$yNSoMYj-LQDcR9aztjhNOS}09TM4* z_OO}w$j$4>K1_6RXiHkf!fuEk?^18PjhH0%`!HTi#-v?q{fC&|Tar z;Dj#Eb#Hl%h0G-LuWh+Zhv;<4WepD1`j5R8e25ymt<%%y-~wbu2S{WgH;IzqqaSI2t;(O#lRcc$mL;@B>EI|fp| zV3UU$l}OoiFJ4Y$moFQ>+319qdhK$L*O!v6sBwvhdw*5`?r4V6LP?FNh zhmGtLAuEx4DF;XTXz(tfyxKfLG_)UPf%CG-RqYo5-mn?=w8&h@EBb`G*)_keTv{|T zd)2ndm|^lUsx|vAk5oz6r!L2_0Kq9FnY5&FW9*4<+N8S^BBpqZHiiEfWH?DRZGKd{ zug@DsB9lLOS1EI>M=+N&o~vnrFD433?4uVuEA(nlp8xmre1sZxUW{|+-u`JgCi9+> zid{Ox!ir3|RZNc1ukkr0KjJbp_Ot{SIEnT2M=@9v!@k@cKNRU#gCs6NO2nYOOpgi8 z|Eh}n1Ry1($i8b$=1)H%0w>`Fr^K#i>oF8kbv|<$K#U_e4WuS}68Qe@c+8OL*wF*& zyTOl{_vPIg5I?s3kbG(nrA-ij1HE*rXw()4$e!ed2L`B@s;J2v>7Eg@l161A)S6v5n?ODu|ukxa1Qr zPN>iut}@n{kY}Iu(IR*Dux{na`F05@(1WIKr$wh{^QlRN@Bi2{P{Ng&Q<&-n^AbHO z?iJpi?1@usnB_4m?IG7|2)lEs3Z$y`uvaL;P`Y6XqrV!EPbrGu8t`ng9h*{jBXNy2 z2OPkreKfIA96W>5S@{`wD&`-*pJqzheZQ$pGejH3(KaB`WGNoMpX!s83I1@Dc!2-h z`?q5ndJ5$0YMtUjil5Qk_`h!035+vsPs$PWd9Q`%&P;ev-g6@Y5(mC3Nr@+E+BoJu zyg$tHSFIH6M)89ePSn~%3$;%SyY~Y1Z=66D4vM?ypd;&-jckghr@T8T-FBl z&R_xXYbO7+QD2Vh4oK6#1Wo|HlGrkSMxSNC0}N zCda}gnqe?NHmj^Ns~IRLg}+41a%|=W;2fpWm0V0{mN2lMya7MZEy>X4a4NP7 z8!j*%e^&j9c**4h)1dzM4lB*LS*DE{YFjRNJ6F|+#obpH+P8t{2K)xBmq=YE-S+%w zLB2-eYEU+T?U!Ff2XDBYvxr)gqol2?R~t#^BTKchqe){f1;`+IJx`SSa{i{k~b#x3v^%9!qqKMrL9C zrV5!K1tVsdyTX_UuDYIzDNzkMn>9?1ZtyriNNmZ(UV?f)cK<3#0h9(h3i0gk{J^AN zv49!tF|Sp1SE6^hg=9SwJ@-6|^K~&g=2_iDk;X-x(xK?o%F7+bdc~6-d1C4h%gc}v-g z3q_Vr)~9W+G5J+`o;t@T^#Tr@ta0+>bHWNcW?{zmKo<7p-J482n_0{cWLC(&cmJY)Y>=2#jm20|R=g*0G(BC~>dTY=o0pySgvxOfv z%LTLhFy?Y!cU%5YXdiWy*yhTkYfXB{6qNhXTUS`B3mzoV3||SLyIVC$dw(3+uc0f}69lCX>8J$~hTlR#v*jiDJwqi@ zK6ijJ1|Jo&b!?my(qGO_IwW z_mLjxt2jx7aN+}9a%4VAmm={)ONFjbQvZzhnoNuDC%^(v%W@8|}C z$%5XF?^?)h#50yf%dMCL^Mo-;9%~frQwPR5ryqqy4M)ndVc-KPk_n@)iMuSWJ!xXN zMaq_VBRvk7HsSQgd}?e0X2E)B;$|?j0-?QoK8sj=8*3;Vx8HtBUOQxSR&*Z0_klSd=t0rt@7cJ*YX2T+@R8GAcI3Jowk$vo6OguiNK3C<2EG^C*t$y_>e4@1RbBcGl0f+5$O|kR@#OwKvy6Ww2 zZt5XyU7FiUiC?1V6irU7|bX6~3F51sY-SU|ChjZ!v}C74x%T z`h3NrT7JToZ(*th!nqyu5m3`O3MS(%!#ED-px3Rk@>bE&YF8c8kfql0jW94XAp|7Z z`FcvraxQIYNq%(&U`+3KHctqW$`;zH>fdD2>0g9Qj75Z?bdnn5UJH8deVaf<&Uz?v8c0gbEX_Z^Lvu=hvGVzb9>=0Y zIJD+)-Fj-+_0cEK>Df6=gQmlFSt;aj2|CIO#^BfhV!K+292HhRhc$L(ycKRuo8XybIwhFwdG)hvv0@%w*!>x&(RJ!q#57Ls8HLRbP?)pzLr3j#n2TGCQ zuvnCcWze_z!tbvqrrNMbPgp|x6tY? z_q}bo>pbTw>&kAoy&w#1vewc2UB~Cb+p{R}`2w(}y0EF*z4bA_A1;QPGIfRkD4FPi z-%QR3O;`nw=F}-05+P#c;vot>=b?v^rVd(Y6&#M%l(sxdiPfcbTNt^=+KCG2K3K59 zB7S+SrZ}!%4!v$i1@GSl` zksa1oTzOIQdn)-#Ob}Vxf;E3EFUN^=hzet$U5g(?f( za(M!eGgfPfR0YfF4AyeY)(6+ml9o$b$OgbTg`u{IAAa}9>&b#+Ys~5E63XtR*%ciU z8@(Ei`D6suZSKh;e(b>3(Gbc+zH(Kmw|!{6hZAJ_Vn#Iz@g9=>9cmqlXu19+S=fUH zmoPE2wghXOs(tNVHi&+)nH`v#s!^lS>+UpX`jPRw2@PT!{N+F*zP5OT95AKyy%?VYpwYxIz%%_qAoF|TS(!QYr#Sy=4O?Hw6 zxmR(0!2imP#v`;wK`sLkiCk$QF`-fIPI2?T0g?zLMvzclm)u&RdjZQfPu7%;@5w{7 z0?x|!>Y}ceLvgr9^LY!x(&6J*y=?2l#f6^ZC1I2&I)8&?Hb*ZbcDy^HS&^`2H z+}4<;&I+|uOeRl0U*;8dHArA79^BhC=;$H# zo0|)ZpVyR(#&-1gn$PeI;bj(v!1coe)6+@EycoZQDd92z#`Wx$elE<=jul%2gjyjg|w`Up|0=Z2(iCz#vqB0Dd4=F3!spLaTFgubjieJ6W zMosj6j#eYe+>er|_iC?RxYDk;Ehj!5<(4Q+G)gNSN7e1-&gZPOtI#MC8FW_+hg0#h zp*e&EH_ItpMLzI1Ws|HB(jg9KKos&+)%1PHGvP@n!QM4#l69U%w;+$d5nSwPP>VF) zv93JJf9u8&11Ub6$j@!YJukja*e`3Q99mrsC#8pgM!WYkbZhnUHr==OWYE)g=9 zJ8#>B?kE(7W^1I2Za*kJQid`I;vl2%sVHWHx~^iGJih>u$uN>V8bHM$OL`*J*^Suk zmB=TN-n7kBO*=}@2ya7Yz4mOt9-6J8JY-BgxZYYc0w-JbrS8sQ+toq)SMZ2?MpJ1E zo^!SP^Zs~?l?gV{qqVj6q1Zn-7#7sn+X9MmEoqG8Ge)x|BK6RtN-y%wqJD)EO+5~ou zR_lVVWwVLwsyC-3d!Np^sLA2En1P?ZLz^r7JaPb_~hD z5KHyqeC(jDDdKTg!=P+i;udM}?KNxtaUf)+q0j^QzCe9RB84iIAonPMX~XTgo*U-w zOchv@P}Ewxq5WGw!2fr*Burnv;;7{~7Z{;6Ua1M-9P21BnSL|lxW6QlLo#S|k%(Gq zWpoYRn|uW6#jD@k5eE5+Qii6n1r7OjMt-x%aSi*B@lnV!qk5JLUX(nd^C?Nmx~L_& zSo>(}gPGM4dzyn$z-HxDy3WD0G}wGHkb71!%f#(nCV{wEj3aTlC*rb$nL=R34htSQ zDfI&PE)ZHG`{*fZf7UD3dY&PWDGc>7dpYT?C;QamlH5rVVZ4y>w0`39>aydY3Ga?b znHO94=cOdKqNU-j_D|Lv90FFd73c3W>Sgt$pQ$jfr5y+{PrdwbE|Sn6|rSlIB0_B?7DIda69q_Kvii-b^`y^W~(_P$nG_ zP9{wI2CqN|zvLIm>w-!Sj*dFvy7j}4#<|GN2{>f<-49o+KFxX_UkG09OwMuzsQ=b_ z6RwBIeT&!Os^1!8IJoPkP=4GkUvj z?cr~FIz277UqP{Y_H3zQ{JnAux)Rj`usFkhL)1%VWAuV<4l9g-SB&FV*vi zTxSE1JjULOic}?pjkrNNXth{%`$XlN%=|;3VM-0V;UMwiC~}5+n`i%H;{D%RtN^YGlJKLw1hdd@x^Inn$os2%Zf-YqNkMiI@PAoGTNic!!<)nPNc7_pf zt<KGTbu`y zD!=f&oHein69EMY+xD(Tm%%WWeeAn0=8*`lk-X;`6nb(gOAkB5mv&W8otzz|ZR8=r zENDEZU!70y%4si*ynOHm$^?>O{1U&ye$1b6=JUENl3Q9Bf1mKd3Li;(fwtCjf$eS5 zqDkXvGp_l<#&Y{$?%DE%+`tH(A%NW>qP(O^Mo%nP2g@~iRJ!bzfQ!QDDwVZS@_HR= zH@&;ASSHr@&4DE3xa`2v0Pll0z(9M6h`mubOv=D~2B(MN;cKc?S5qVf2g2+G$ez z@3{)0N@YY)Z%WUN@OtF(nI@2bkq-~2pdFRnPsVbtk3-z4h9h694q!bxxM;d11vl@x zATU}wQ{YKg4-FFRL;f&Ro^ccp;5_^S$+vjU?p}A!zpDCePO9UL59z{Pk}nxWKQ z?EU?8$i0MhJl6L!-G_MJEs+Ys)m}ukGPr0xk+O|#aJ;?Qn$hl=0zSeU%1?sopLjhV z?v5r?_n`CKB6O0g0g1^*9T11hBHk9R9^f5APgLUq=czUJuRJNfY;Z(@moFO6a0=9` zOf;T=JJZO>?z~2F%?2ugUAj@x(A4r6oqf^tY!n9VaJG`5ojlfDd%y0@ZBNBZ((lu5FO)YbmRwZ8c5_1oKvdmF{2Rr`Uf_{f+s3EL;} z1S>I&j1enp=X`??N@2{ul{d{rns`n!Z}-){@Z}-s8!@_^cfxu9on-SZ-^`}v<^@oD z-z5KDFXx)(2fx@!R_X!qmuOER2HWo?$2d6JI(#kXcw%MXPTH^mR9y=~-Hm70wiqkwHfCx)K0#-Hj|Zema340l+QSo{ zO5HFny4#vT194@{S8?qmK=}|P5jDFt8Ff(JJ&!dgx}IU z6icAs7@j%e-RD%NIvSj6p{BK+Az4JU2PBdwsP{iC1qE;3oj)7=@Mc?9z$J$j8sdL95EcyON5#rxy_YPvLA;s1E z`0H<-m7DL{n6XdqI#4Wrpk@ojFfT9xgJAT$_ilcd9E+_wea;Tk9BJv@7q_CoPB0Up zC{0}JV28dI=$X5Q29K5Qw-Zmk`T>i}we;KFim1Ewg)+>U6XV(5!K3aO52r^=o{4*K zuD5?;F>aNhCVM-G)C;r(>ekX{y!JTPcb2M%9LBsa)63Y&Xqn@rK4RC(%JY4p0h7y! zy~du=zWrphrvgS4zr|r1L*9KZdCHd+N8%b5`u6mr%YWJ?+g_W*9e2r=<~cN_2|nn} zlA9>p$3>|P<+)Tj?akl9-ug1UB)yI$!Rq5Rr={YC<))Y505;ipDJbGPS9R0K3hUwfT+C=4mTk8{e1yIz>;6b+XRsOqp&BiLBj z`3k2{w}>#r5K6)d3x+vcMYB4fCKK{}& zPouH+$=wc4>jo9oDoE+VKIRV*lIekP%o!H&Z>fCI1}ZFwnAcy~o+W+n3# zkWAo+G(7;!l2GoY1-Ug!r#y8MliZaCCKeMi2Xg3170n?b4w%5ZxU9q^h{T+)Ndvfg z70rEH@J#zD;el&Wf1W{t=3TH+G0>s)kjxzM{idNO8h6=c=3)F3$4kiYzIk5&OGPbX z%;D|8xy;nFS7o+A2woDn%kHL{5Ok7Y>2g0@rWD-zW2XYqT&GwF*>sxR-*xP|2h@~&*k6@(& zOZk(NFUvcIb!&QhzEDDaYbv0WE=CVWijYubcg4BhF z+O0y(h^rXW{LEAzi%zOG^Nx=pu5F!`IYW|}h>8o#db=VE=m>!;4sv)%B01Xg>;?6G zKok`y+$vxOonDgK>#3Hfmc2Q(+UqQquL{a#%?^`ddNFjQ?lRq}IMxgdzH6^?rK;c# z1IOzGfQr|;oU8R*3fS-L0X<|(U;8;sDmM!6!F?wbiXaPI4{@J2J-9!z9yi{Rf4Z57 zwCfXDr3raAk+@790xuq+D=-s~D2!M>muA=a1Uf`rJ#X(i6^HsCc-60dfK7Q0E?BAZ z_1E5!YkJvNQ8#j|gM9B;F8~SX6%|BX#2v4n8MO-B1hrvwJaB+l@(#Y1Y8=~F%==Op z)iS4tM$GQvY!c>BU0OjRGxRkbXICYrq}hZM9iOiIT|8W(0tbM&)2upHV2DYp(M%u+ zc#bf;y75&D8hqmA$8h;8@017;I(a9kyECkp$W7Vna$i@nw^-XJ=IK*pn5;8hY5g#L z?XIy#Z#YZvw`W@Y_lNiJH64f_8h)>~Nm=DNPj6Z2DAZSiGYz_b zsNy@3Ln|?mTUx6yaAPSLa@YB=nMo1k>MiHa!-+g_Peqjc-71i>M_rQ=8G&I>rW<)fGj>q zo2ia@t+Rg?o@t`6^<`;yS_Q=GCs_#512XOpxnw?LRV9Qv0uo z5r;|nLnb72i}CiJ2an&*&q}*Z@bG5%=$R^VAMI2TfqrYe^FIElaT=z#-N|orNq@6L z#m~spb{$RMe0!U^{=dELJ9t5V`P^m+5glZ1+HA_NZs-zxzTe0}Pz2i2xmLGeV60Mr z85>eVLpUn^$@~R1MpUTLH$dL=)NPYKB9kid9n?x-vR4A853yUT4|+|FzGUofz+%v} z?!;m88H5DWR{jR&HpvVR#%LE^on&SNy{w5_D5>V(P8uGRydg@w%B+6#A={=x5GHL+ zk=vTZXp1wJjbfUKQ>BFH;CAEjX&yK~s_I=$0~4@FA0OA{Ns1WC&dpnFLD3il1Vz~l zJtJt~v3poi92IEr2Tg6L=0Gm{H!VLAf0~OST(y=QmDHUJD=3Uk-D;Zu$o{zER5SOc zSz*upqfpanJ#>>En0po!gq-1tnO)`8wTKS!+;yDx^)vM|a6C2v2qYdYuIwCBu@+O9 z&2{1V#SQk&w&xanlGg^tX}`EPBmd**%gix~XE(w>itEXHWoCa^nqoXYuB}ZB ztK^G=zOC;O8#xnV5WNwt)EYCuZm-ewj@c&MOM}8o=>T)7F_f@x9nDiJ{jylDB_{Y= z4ry~=rQE+3uCVU##p2{C)YXvUSbThQ#HoFu;!P(l)P}gvfD4=XW*k6jj zyRy2YZFtzydP(DPue9CwtUuW<-^kp|r+0Y!vbjAyY$Ep;Et2jdU;XyH>bLc}l;U`J zhUNx%x2-JsZh7qiY%bt5YCCP(n|$AgEYd~ z$BZiB@Q6Bf6A>&uS>MtCLwKRVT!M)<*h5TeMnP8B4stbd7-6#uJ8_Opb2+B*9d|5v z{fo#F)B+7&TC(*B18g%}Vd=xDLQP)MlAgN9D&P9R(T`dsltUT&#`xTogG0{7u{qmH zO1~2kfrnj{QJc%Y_e*rbYF%Qud75)r-freE5bO=VWa$~X$p8h++73NBDI^}>!|g^z z8dpo+Qms-fIzxDN=bB$nH<@ZM5!8}JIsG%sa$sngEqG`zk!iF~sFo$z$03#AoSI!H z1}@3cpi;q{g$4dnwaeO_lnqm|t{^%hGrxW{$ruS3DY{$p;yaLFrLDW%=9ZRKqIO+h z+_$G&$%^939J2?#_ER>5NuEMl+E#BaDhpe5P>G#Gq#=BRI4chatLc4mIQ$!bP5)yf z2S&USZoqo0&?_KiKZnZYD-esIZ1v??Es8cz>l<$_-DAydYI3vhG*k$yi(S3u!hHU7 z-j1$!j#`vrBV_;FquU+A!C*R{IyLzQO;c@ccErj-a2I3{H)A&@aW`i*t(lgCYM%#j z;@#xW*ZK6IF+AWf7Jw~CUgdUIlKhh5Hsz}(@8tMV^F(FxkvEdc*H04nU*9B7yUNIw zk!d|u_Fj6;n|rd3k4%tO+47KeD~}x+aN)@c`T+lMdcm_pES(}D`0-;HdWeqzU*_V_ z(sa)7Z5dA3E*C@i(z0KD(w~Y&nqiE_mL6dkD!`Tgog znr|2P763hRxO#C(tYbkmspSy>zEE9v@7Izxv5 zJ}sL99z!d{YzYD`i_t;;v7gF%j$hWlobb5F5eRiyXhN;r<|Q@(z2mr0J>6z{(0I03 zOGhV#uUDHxeI$uD98Y_=i4evp3qQZUkD0l#*{VoGycxXAX@f6S+pWjpY+zk}V&(bK z#zNIrTqXXyImqIT|D3+O)@1#KA1W{z+AN9c31@+&t^0;GGWIIMNfcD8A=D z5&zNRNVZItdg@P7Rw-*p(VrJz`r|0!3>KCS2-EU76*`I-j~#v{G}U_X-?7ym@HX87 zeP5ElbB{e8W79TSb-&FbWtP$NSf(ey?#s}vd(D}OMwLZP4Wli7x(HT?^uMCb7unwj z0=W@7KZc2PM*V5T2XMNVo!Z?!F z`2G`U*BFxbrsC#}pV5=Q{kWOxS`I@+6A^isf2W?!5VA$mH+ex$r|UZVFD8=R(=UF^ zp3aWh7wa`MR?@hf%|7pifGjy?=1M>MY1iLPH*Io;{isXQ(3PXPWZ) zj9FEh8eh%bN(+i}QUSe_6ZkstgF*aA>G1oxNwd$QI_9VMs_!)L`%RD$YJS>zo~DmTt2y zOOk=5O*ZF7;o38d`^O(vJ1A?mTz2}YcKX$bYYf~<-&Q6Clvgj(74;4re3w3D5oY6x z0$(5@@a}&qfxObejOJN6dUtwtc|MC>*Hk2~x;V{rUi$a^BI4Nkd2lj#%2ODIoI^yG zN`&6dfBQ2x%=z{dRs?#sV_hF+OIq?{V6~J+BQ;V|++3-u#l@`tNgf8}hHUyBVrU2V z+0vVD<2jW2dro{r)H+1g<~r%#W~z>-@m6+XEm7$j<3P_ z(_Q@r>p=*Vt14lo)~eQ&aR=r)A_fGmh{`5_&9PZj{PG{Ei4;BAXVy102vy&-e1ygb z|MXxZ&glW8hpJq&Fa^*e9@_Gv`#Eiy0d6k*n#zexqrn`LEQH zU$T>5J|y?EE^uULne%ZZwoT?AM4=+Y`rNonjh&?>)hWLEU^iWU!}H$AyJ*wZ23TtS zQ3Fu{&yc!pJ7m8x=V6aZXgfWm_mhvrrv|5(n$)CWZp~KvbmsxEmZYY}I$4nIiQ|E0MQDI@$`a86p>`nB2FNPk4Eg3^5Pt^tD+fn--3;=n z-rc~q;!2$u7CJm9k0O>=UWBtWUZ}vRmn@i7F+{8vOL0Kal>+G?mx1rXneBc*bKzb= zkjOZ-;QTdj|Fjdlb*9xY?280zcKX!<(0#K-N?#HF>$VWWlyDC=o zCfuE2FET1;5O#3xFR!H!eYy?=7uHsFf%%urj5FB*U(igPk({mIh4NSR2*9Q~-kt1P z+~C~)!uQ+YSuAv{MFBl~tEt+d)a1(}K+Mv2&!%`#(cz?hPB?6Sp> z;0*eiKtlD1xlM1!E~T45?nrIaO=odzk_9OImxo^KS{{1AxbM$JM}1*~!XFqYS*XH{R{2>ro+mJOTvmeS)EqH#UaI_-iUu~1D@Nu zj@Ql2U;x6tTMJs;tw0=TR$Ad6R&i^+fqN;`RIWUZF?}l zWYY;o{V_JZRRztHH}{N93)>k&vUscAowY9Y75zKlRA`lT8EKSH2ey9OnUJgS7)hwy z4jMAn91MHu0`{v-_gI`x_Y2^T#8z;fjctp7zc0?48QQTsD+Nw{e`Civ{uDD~aG z&>6KEJ4$7CTzm3o?vYyi0?u)2wnV5Nl{}fPx=~!|?se}d( zvUTmK4x|<_x10I#b!2-pCJ&b3J5#*uFYPPL8}fF1g0d=kI%B&u#AiRs{IBkTMIuWNfz)5ha)x2qRG8?n6e&R-jh(P;E|1#Kx|I0JRt9P0^M5!-_ zRz#zY-)Jz^9eim8KUK?^eU(CCO5{;UUuD8@y%;235(SsJ81wX4ea_dEO{vtKOgsUm zk{2+3`ExD(#gUaHZJ7qH3!B_r9)&I=K6aIp8bO#pI@G& zSXi0I3WlSUuI3>9g>U)$sesXM?&}|sq&(oZky((Z)l;q3g`4A%ovr$+b=48s-gf(+ zDXwHn0D~G+*4n`SuP<6e!9oe}l)VoE0dw7+o5J-RJ`?_j_3~MHa4HT%-otT-r zC5Meg=xRZ7%b;?aRM)&Oic0>m4+0uJN}B5J!1|04gb;qq7Tx!&{T}5Y*XQ}T_+0!4 zM(AEyqq{8=gn+VRB~HVED?l zZN*!dw4GWsPyS82Sbc&(d!h!K<{ZV8sG?}7>^_i@(5P3kYdE+aX~BcWy=efkd4AkF z-W$aKecZ9qr1^#%Fn`RgxAJrSO7^+-LRkQTs`Sd4?#VLlIgn&0w)DA`IMb6b%5Brq z$%E<4gW#U9Fwb1&3Obvl$PD79gKXSeCXLL$chXbcJrJK0qAJKK9pt2RI*oX;g;_5) z$J>?3e6t(?r}=qV+SPSr@Z@B!FA{!t)C@)CapQF}t4AIOBz4EE*lHWd6b|+Eu&L?% z+Qp~bBatCB;_Z6pa3I(JBf&h9a-~9~B(3O3O-u5yy$OLT&SZ50#gxI>du1x~`sE-7 zy^(HiJO1&2LAsM?V&>U_!Lgkm0H)12bE{UeThv13(|B|}D5xzz*?| zNh1FTrNN^_pU_AOMAfOvPEN>p$FUZRU4q}nC8Lx;I;xYvYu)vNqC4j2?63UK$-!uG zfAf3`un>o6TU81nrbk-)vesHWPT!u*qyu+R@uX7z##=Q%@2qa;%v9~I9;d1q`0f_L z<`8!6K5XkiwchxO6FR+Tw^it3nSU`|ncowQir|+n?$38GuPWzt zQE=Zw|5@kJIPxd2aq7=x?&jqJ2&_?7gm->7n4Y$|3bJ?s8;zrHT0yJbVLU7_d|k(W z6Iu?EREoQ`$2pOW(k~V><`4yIj&&l#ikojlLcuJxbH|5|BpqqGmh%?l-%38}6yI7L zpDS!vVb+Z>h7FVan$m($1&x#r5xnU74^ouTij)Y6)7b4{d978gT1D}l8XC8!U(OiV zoD&yQ>ME-ZmToeii8LJfOV?NhE!i0{r@Kw~n%Dh(3`sM=B3ST{G&I2Ymyc`YAzi|c1?IgJ)Q9uF90diLo>@KjHWS;uYW4ELR=0>+ufk|wSM88ss^XRS$ zL2(;@+QnPj?^$&<^eo#XI?R#%IFBW3rrpfoEH`#)irTO~F+qL8Vm!FBR^_83FSeqP zcOTg26;nMa@~pcgwQ%0JMGp0`)Lc=m^NhQ6VV`JY?q`zK{0nY zELyA?TSePd{@T?Dhb+v4#JU;sU7Y+*V|o|4InxNaCdVRbQkaE?Z&p zsYDyA7NlwF+$)@a@ZYh&4xoead~qO2AR4#Py!7WDELx-#hj5K@=nw#DXVo~TN!ai; z*Nfflj5{}EVg3-i@=h_<|6r;(()6L*X+q?T-7Z+*)ao_Ol;Q8RvJ;}v%E;4~;q?b* zOOx>}wQ0WDjKL7*CyIy{M_xaQqLodkckrl&_@XmR#b4o>5=X1r2|6k6k~)v|?F^z# zfBEO(a={(3<#fMSxp0K_rR4LyECWElrF8?epPZhl(%hbZe9C2OBx~nN^>QXgm?gl) zN6{*Jw^DPHACxLclfS-`J;tXeo`dSmb{fSl=iFLR)R_#aKoK z*EZ(CZsYFi{R4LperkEKlPaQR>;Dd-t2eK2A|GS@wg)lIJa4Nk3{qO#rAl#Dv;N>{QW#%sl;+YTg$Sa zi{rF5AxOBNaa%Us80pW!UMcgt*~`pa#j0+%m!UP%vD%h(4~=Dxz5-IXLkY?AYx2s} zSzkfK+ony`+ zQ?9uvd{H`JZq#3~jy-3N_!{P}%v}76nyn^Eb=E(|fc_IC>pK>7gT$88$)qm zRJ19A5O+HibSz9kxBlx+E~)0GPu9RkIno$-@NdZUm%>=J%v(v4hSAL#rG%1^W&y}y z#397(JA}`w@EM+A+5{99mj+)qjVJHY8hqGKus|r_x_` zVs>daHCR-^G$CC!Dv80{V`Z~CsBPrE!6DN+=G?0v1KjSF%O8HeZ`6g+!{w;F<;E-I zbt-Bxos?`K|IEw(y`mniviZZFaz5=HzR?>77>&#wn|>wem6Ja5N-};>&x+u~pQgiB z;@x=`=7(~IuMV;1z5>Vyn*7O+%QD`nL4x693nNBHiD1Gb*SiC}z0iHuZ)}0!rG0U4 zRH-N2Or70UO)m4QiF$>Vzptd;Gi28)0P27 zK*OP#W?U%zS|w)fLZ|<8ZiD|*avNu}rr2P6b6%G#Z&)LJP6En5w*7^|+$PHof55P0 z7&EEB-`r5_m!c%&3QJtl%674(ri)2M@a3lR;mjQ(5O~$aF&{yxvcJuGmLzqyac;WP zl)z}`LRy0|xkGYx)1aFM@()M{ezPKl@jq67-yrDl*Yw!w2@_^Q{F`=<*|{E**FA)q z2IXvlQ{2^Ss;?^3Td}$3$2qDfr2h27R<+LTT4o8B?HJ!TiD`IqMp*L;Mfc~8Wqcbg z1#(my16_v97N(Qgr>YJ(T=bmFrr*!DOx-m$m01XhAF!i;u#xJ5F6BlMncmI-QwzsA zbsfS*eXOf*0pKP4p(S>qrh`eEPXLVaJg-y^^Z_3jcfCxdk?4C}tVs4%JyLFxF!k-8 zRY+4d8OgFmz_jIS#eR;AKzthEiCVkO4J zV`}>^&M^H@bm{px-TBn>waWDwLlj?D42iyr3`LU*?iCFhj-fJQxHGP$JHjm0X9H3C zN&F~7>q9w6C#A_lA#ApwJMyiE&y24C3R2?LoF@jKf(pP!fs3!76a4BMM@{(?9=e zjRZ*`1IX_1tcxFB5@nGJNXA5RtCgIqN;;D@Uka9X(Quz;_wJW0!j18sxXb&lKECaS z!)83zkMEmd*%)7;Ivoj2wS$N*13CK4ERi+D&93&nZ+@JEQJPJai*ym|F6G*HbJBxo zq#(^=HSACUhFF`UeO1W$EpIV)=n*eY0#VtZg-uG9AcT7glxr!s%sB1wNvZpAlqBS} zD;X%_L!~A$>@poBn&)K{s>8^jq-kA#5rPrBHqipt^gqg*GJmPXOtD4=-mRqn^G};SWza1I?hkLfpXHc9j<^p_<{>Oa( zGGZ@Vntc39Bp@r{JEBxel0<(-lB>>o@>Ypxt)=Mi`+9-1Op*`9Goy!ROGT^mYR-KT zHIPP)(=76)AJB+12mpxj@*Z9Llmpu@I`dJL0!rwo3^SulWCZEY3CR-Q^x9)zbxX|0 zPGuof+CP0x=Zeu}5~a)egQPu7i_inChNSsNYH$;$5VrJ@(%s`#`+M97hd^-K&2;+L znik+5?7xS*urSeuglaJ}}egBYiDLi&4L{VMF7$IDQnrifn_zulUBc-{HaLrCR4|vDB|b848adJS~%= z`Q*(W@I2#9!R}*ADA=mzR0niVHFG?%y`EM6Ul_82zS4GJ6+ZXqTSv+rQ%?t?I&n2W zfu;VaSqQvmj9-)C>zl9J<5@lnxZ@NhNg4bbDYaRaB?&{=&j!3M-ypQ4Y|uG_PY@SRfj3*u#L}9&N3fL9OJa*#T#F7{R!)(l$OxXR(ViTRPYNh0a`anz`|` zuwHzb0?-wqBlvmIxdGrg5dfGDDtXtl1s=Sn>M=M zTeT%?s-P<3YL1AiBfT~NY+rmK6Lwb?<{u6ZG||{T9c2IMRz#%BFlY;(ReQ$dXN_!e zDm;GFt0MJC-xoq1VE!uF84dfa2V2Fp(7YRK1?XC%dmT`n`V(!l(-7eXWfR;!e5awT zb(|B;g^?B$bzAE4HYX;(eA$OD7&^Z8OoVLlO8%gDXf;9=o3+{N6xC#>@_GnIOUzLU}VCK2M1H&UvqR1&{$E(HAE?Ch`L z7SrhT>zHV}DGwD0f@x1ipoop^+in=|Dr4fxAPyN@Z}OpVxKIb~e1=4qEC3o3v)TrX zvyC17M(CTZ9ZM;c{_eW?R=|_~O1h)@Yr`_z6-~xo*WBPj^0$ZhiDHNla2x=3pl)Ko zod?J|w93{j4R zv0KNr70pS1B8zNl*k9e&Ifi~LohRV&d=H$SPp(I0TKo@?HNuF+=GaVGVzt~lhmAtv|gU-vWeRU#Lh^3f6Y`!-VfAuqb z@R7!9*=-+W++PT*y#3oYyOoVT2jh1Rm44{Ks^f%Rm6oPxWG)C*Dc}If0>%dG6#w&f z2|S=PLuF_$*-F&3m5_7od48L3=R4cwX-v|nwuEslT738wm8Q2#szU>wxJzIJR6`m= zlb_Xf9ruQA@bs$u=1+56?=Y!kTRh%afmxHR|Ey*T^THo9z&M@6~%`bh6e}>0ZZXY>m+WGdnVVSlgolGN$qtM;*R0oU4G? zWkt}wH#cD;AO&vu?$CL&0Gjs2|LvjoN!WBBAHyBi&$I6CCu+3EjrLT|lDnEdjU#?3 zN@DKM3FA)>>GWP1tg>0Vaee>BJc_AiCHjUXl2L&-vG%XpiA|CQ-^c>d}e#0NmCJgI?`z0_mW@@UwD7- zs%-&Rcr#?xz;2I4_SUq5FcDn^0 zw^B&ZZKsqQE4JGE4wA8)?9mD!nTT&Jmsc<>-SlHJ3|Rkk@&y6P#{Sr2%u1ALWso4KVio)NlPv47T6-ua}=L zMG5#k>b31xbM2!(!B2E;Rc8wap%LivQT2zdB$+~XkzvF)yB&NfjVxAe2KFO@hQqa4)ujCVKJ@>Uv@0K5imZrRcq_7~*i@X%0o72t4BxkPmpI80va{Pn5X>6GgoB@pa zf{~##*{S#Q1uKWc9QP`2s*dVT(24_ZN5iAmmWHeJd!_DHVB>DV*gT;l#vQi{W)gY> z(ra6{0bK$SoLYZ0L}cCqtBrX9%_-oM01QgMFu!xJM1RT0bA|i(RbI~|GKpVF8A;8G zh60}sTuRJD`PE@8eLp`V_rGH*reMg7o6%?S`;Rz7G#lRl8rg%f?p?-A2)C)k;5J2lLmT42H8=PW9B5JSK$=a4tOvqd}*UXg6i66 zxvecfrSiEjSYLMibZ^&DQme*);>)NTQqlW z5o#iezqU>M^r&?@zuf2C`T$EkZ-(PgU+aB|$|FDVa*0_=JxMeZ5?^N^k=+dAUotM* zQ8qGt(r~)%MDhN$7(Y`=n7R0$)}@Q44FpqI_WA@^t7$V6?j11ZKD9;kkp0uZh)8FQ zgn23xd0?0}nDlZnRc%$53gU%^>QqFW<{u;)OIU6@$p;%}mO;`vO@C;dOCN!(XK0LY zxTzqO2ybSir$&uVoli^mS>17^U%E3{0?CKIMi?WOJsVuXjcMX%o28n=JU0jC^{$>$ zbDp{KOI!dF4HWc+=?@>esG_fmHW$j`Ps^IQ`+Uk_o5!BgDVnWY46GeS-rui1Jp_rkiJm&* zyIHiK3g)9{YPrtZ=VeMCIW$ba4rqRW*(Mx$U%8(a={*%vfC9;fxmOWxrq6~Gs2E4`Z7& zZ*ykT(l;FhQce^f*ZEtO_--qzU>+VP$xMB>PQ&x_fNXE;^eWwsY(bG$a?6hWZZ{`c?Zn)YO8=5RlSzeljiJc zhZDQ_LpL%zPPiq4Mw8)|Y`i&%tG|23;wk)nU&}+9&*E?7=P3FYOs}8-;3W9zm*_wJ zP)LM8;t&Tn7||t#IAy^6D_h>_*T&cNzVD44=&Yc|42TCY0t;5%=0~Z@d3oqU9ntjU ztZjd%?)RcQhbkUZH=j5{Jn{Q~wFM9Y{Y{|^>A+cjOmO|{qycxzX$~mm|KR7(irkgb z!^#1Vl-V{tFi7s@7&KY9Ql=uwEd^p;#4mJ7f{B#Wv+0$!s|p;#(ZVk2T8@tq_0(xV z0DVhK1q8)f;_@Y7a{Pc3B$uE{54>fN!dOZIlED5zAxy1@iA=67M>CnN4#=|wb&u1Wy5NlEMC|%a4G`rS8KgDHrHzy3wQR}G=_zA zpw(R^JXj}LEmn5%ZY_S`UzKmLZ;z{`ss$AnXz_D+w|cs?`@I5UNWz!$FE4WwbSQ(h zc9*PVyc@H6&Y@eqfv4w7+(b~oee>TJ$hf3A~c7R zYo(Lzzonpv5)p+TMd;Y`^C2Z?CMOL=<`eBi5ivcd$$+oy%?eq0`G>ZCXoUlwg|lZ| za2Xmgj1dr}pe%2M0WZR?A@m+8YETUtz2~S_cy~DnTX0-Xy=~1YvgEx`Th=XndPFO4 zzYDFLX=QXr z+QD7@s@lgL^7DsMBK++;VQ4+sPxm)-v)RJ$daQ2L!7OBhLjr?1DGP^la-#AbC!uGd zkcw$1JnTzd{-DiG+Rmvup8H(GcSIw=AYoKOZlToN^V zA#7ZGPgch!-#Db5dmRiyJKJA0SU1;VWZovaw?hdF*PLbbo1KHq-f!)fb!-i$G@yVV z-=u<>$Ci0aQfSpzb7+~y?#bqyc1Ga-@|?XZE2JXZ`vDHIgUUkckyq6SIR4DrwtQ;* zVx?!2)4Q=@B=ASKNVO#6>d2eqR0O14v2N7V^kFul>(yuk-TZ?mN$Ry$giwpDO!fYW zAiw|pV*C%SjO$>Ymw$Z^qOx>IcN*FlF<=Y);zndZ=>QevBaD=f^FA<4=HO4ijVPg? zgryiIr9n^%i3wZBzFlXiRj?k2MB6!I>c=meK)o7)bz>EfhBcDiRiBG81#0y&-8L@v zrHD{FUe5E^J(7}oU!NqD_Sfv4hMCfJy)TIt#<+PLrrS0N4w4)EPsP52yTSYLgTOj* zUR}8+Mv{P&mG@VF$u#E;F&yl#-DDiRwz|j%EW&(y1}X^_vxKr0ruNVf?N)RrNXi3Q1;EPeJ70$jO8h^~PwmL_+k9;UdC|&uOMVAQjnTLy^o2*&ICe3cUs- zoNnF78A)CzmQsoLcbbuFBX0uqb0@5lx|ly)k4IE0P#+Q!C4| z25BiLTVhQkArEhM{4x};q0J5aq}eQ&&d2OH!`#YmaPPwDbzdKEDr$il7!>&dj^lg> z?&)^zt&3?FvRHjnXS5kEF)?aC9!6<>{Rc{B<0e^1lfYT*R@6w&8$v_&!)5Omk1fVw z1yag%DU>p4@6301k58qT1QiwES4&vG!DVrm-QH|xYG{apKu`h$1OGT$_uE-apr5L9 zIeHc$t8y0fg|T{vfFqf+RxW;0UlV}7k1$vX3c}fn$yE!BHjRxbei%FrOH^DP&lW%P z93`=NMT7pCn8kwiU4t{yxlyUx)lt7MuAYaW9;?w>zFF`F_q!kdN~_2e^D4A+t2j^K zlG(RZbaAjkL|up(?kpc%Y06}mK=nNHv3F4+C;G=M$V<}3`Zwfr4KDUQtU^BjEDfd@ zQMf1@q7*loOJRBq+`fsSNb8ru8dQIYXsuV^MXhqz^krJXsMv%Xc0C(#Wrb0_>%Z;X zU{?j)-7B06d{neeEkV3%(!Ri#?cu;-(+K33W*9q(Rx7oPkdoUi64On9{w-Wv1m#Xv z6$X}Q^rd0L@BV#|$>cZ>Zy)ykAO7{W96bn}EtRn`4?Ugup8RG-&4_B_C)a!3MT`Nk zAyb)tjl~$meNOocCB5lyDreN`n99b?GNOJgbVXmUpd>sAeS0B=>?-ECr%WG|Z_0m$ zO8ne)BqT-7Bs#8_G9yBhnMvo%FP+v^cQ2wD9g@VYtQ1OHvbOs6O)1ZW<0U@371Eu< zWrwubAED?3q{Q@i-As{&Gbkh~-CNV>#2PvTSv{dj^Dz{U{BvF~o>8AOLp6FBZ8E4& z`SA~g-{3v@P-Bfhm{Y1%R{ZI zG*BVa%{!><4y!_ZwqY}ucSo(`ecv_?n~s7WUn2hI(DOzqt2{;Hy=PvSsWNRLnia8j#kQZGnhFm=h^DX2 z4>Me9zr#{F5s>pRnuhrEg7boGg%rMZA#`Mo;01;xAD{t!LquFd?&z z^7h+L2d{>zKOrFbHVO;0QcZf_lNALc+^!8AQg5E6;_8~r5D~@sO?U*QlYYN-=f8)hY7yISDP#gC$f+viC>vnWnf2SUIC&(7pS`|EOh|wm@ zz~6E_UbSM4Y!NlvHDU*O~i59O+S%nw-= zkNXf|Lh?#k@f_Rl;(om!wXcj9;Xu2os|H$HI`|mT`d0)r^{dz-*mFN_=U|S!?qdiT zmXtCkF2)CMXBXZ;KKFBXeDyd}7@QJF`ID4=JrI|amKND$(BlWq=;m=_D;BJ@@z&x) zm+}lV>Fi;OZL4qk)&0O5FFZUofoYq?`Y*s`uG{*5s|%zJju4pk_w|&OS(_U@@R)*( z1S!&COH~BOvfIL6!?5yprUewhXDfibKJgtrSI??gb- z{3nLQ2g#IMZQP@!7g7+if|nmH-jB+9t1B#^A%vK1zgAa{{hv1B_dL?|U5$yS*1|8n z@-?d_9fQ7kaS9C*D+HsubQ=oCB!jG5PdDR=UMuOuQpKGPM~-kAQREzlk3;WY0Y7a% z4g8l;)cMcuv+6DxNcKzfZ@cEtEao63Zl`QxGTR1PFEfPxX@St;FL2=Wm<~o?8Tf}F zqAzFCRJTSJs9qzvpmgfz*#}!neF%v5SZeh5SU%(zEbcgUQ!7^e>YzSF^NJICi?M-M zjndIeLfgX%txGSK(0@`vpkq+NeG{vmO-`|!Je}oxnazepj-l5^^m#zQ?yV~SpC$R( zzV4d1A{D`}g}pOjvRHV9yLTrED|AC|-PK^L)_udwtr4#r)mn^ap70JbAtOS+r$3X5 zg>?%J-D;0pR#0!pTa$`_;tF+Sw0nZO#XhWrgogyWA2z&h)OIG48)oVWHkOJ9?gzwf zlM^gH0l04W_O(Nly}KP#~DgTYiMXQbNW2C z7fPp3??4mAXfm2NTV;ZGItWhYXv3nEaV7rq=g$hPZpn{9LnA0Fd>vE&Pv@Jc)YNyT z!)bxNm{hcjJ)vKB`q<@D%Nal-O_h~jI8XM=jvp?qk00GF8@^`i;i5sHDp4@`(TJvmO`|5I za!cB1SRp3N&0_?u$U+1JUHq%cD*Ggi2S+{^czQ54^UqsNitrApM+Ee%z^&rPm(BVp zquO%Xy`Vl-(KRx1$@~GvaA8u?n8s%NpTW0BN^E?oEOk*l8|#SHXYVC3VY$z($f3~h zgEZyZ_j-&XY!Pd{HLkMCklrw@$cUk;iwmr^aib( z4`-b`^V=RZb=pcoBuqNFXPN70fA7W(7>)hpL3CY4X7QXA;CDn(<#$UF{&X02{>{ni z&R?+n_N1GVBO&gjkSTy zyn1UB5{N;6_uKG8;a}8cjS;VH0?!x_M5+z9dTjJY|BPiR`c%?hm8z=l?*|753b&=) z3O}_zkbU_QP1j?6eJZ71WrBkBc`b>5^%8U#06J`bd3mrjI5~ZOXyS$WxkyvR zFkca&n1LXi8n&`Yr3m&YYH!3DF8zwDR06$ki8LO1N0fW8z_QmvLo^ZXoI(DuNE5O` zWHWwdrk|J$#@!H7XeG`=^`M@mT1aoa5@^mi%oM60sL`B~` zFW{sI&5=HP2C{6l6mz1T0ZlLyVLgh0m)wWnQUa^;mx9(EkhQhrBPt_u)wZs=V z*S5{(1er07|vQOGbub)i$Z5J6UhWfAww=W!;@Ivc7Aw&A~uF#g^<#A+h>FO{gqOUdj!ppL1Onp#^Fpw z2Uw@bFCTohce<&~VEYjKq;KKj&4JYxQn2^{G$k3v7E)=%NU~W5yrYL0TZc82f}+(O zU}BeJ!JWybI_5 zbeJl>+p(x<&LJ#K`;NU_ce^e6iqme{KY&r0pi`dUt7!XPw}n@8YVh^s-{zh~gd&m# zJtkOx0EKc*Q==WCM?%}%R-W@;2Gpi#=^O`lRpaFv)tU7w{Jv@K?}$_mdn^TQ^q@-N zzGDYuK5gy~Y(uyeEb@vCyhW)r*!fvRb>s)m(zBlz^yO~??WvhcTPZE5JHaE?R(-iN z%-{i+kcd#fDpAq$(?w@8%W=vE0Jf{2QSZ^N?g6{fyBzdjB(j=E!?@j4nj3y{Ud9NE zfLQe(|GXOEEd6}H?vJ+ma5ZhgULzstyPX(5Iy$;gbI=z@_wtVWlc z*_C{deZB1eBkZf=qTJTD1wjy%5CLfv5NV}5L_s8_YXGIY2N*gOq#KC=lyGQLYdufvq#F2s$IJ0h^=&aVSrN&=ga*gw zm<7x8#etYf4}!}+PP@MrQ(M8-5az5(J^ik(-YI{(BJTnusyPyIOc9pg@>qJR}&J_qt&s^2&Q3Wehz7taQz zY2~T};w5nDmYqj;E1>{`G$g>NjKo--_VW%juch6tgTL~=CkA311oim&9{Tuim`-~;TI`u8_!z;`M0|)<%Y~he?>=}2&!a89#x2{qNg&pCAci{Ro@_NxFl@*%l5yM5 z&vmOe>k_f)Uu)cHxN|?5$B)+M6ob!hzNm^|{4{|L)OVoogXEoziSJxChgWTm{`?xs z`OyHMD|HPgBKBDZ-zRXMAwR#9SOWdG>s&$SfPzQf|_OS-Qh0MBKeKgAmh zca$FL-M*GuqBdJt^m*Ilh?+~aiSV&5bDK&(Jrp@QwQtGx`GQvK3o9`To7Z>xie-YmD5E7M^O5Jug$w$*D>g$BG>y)feAd`t~fvXE~O z6%&n!{u=tKVv}$9l}RC){JG*yq|5^89||iA^_>!>oN!%gpkvJy8p)X}e=GPqHUP%d zHi-L^o4jDQ7si-(e5lojOx`>(yL=mJ#PH;KSJ`f;mf6#ynQ!Y4?p7DyAS?2Amdbnz zl!%iO5*pWBh4(kQg8&yE@Rm=6Z@wNM!eh5v&ku&Wf#N?Q1o|W*k%o^~gTI2Zzg;y% zy>irt@4(yX2_g)QYb&35b*c3T>wihmO?gN+WwX=d-W19#4?^~Qk@~n}+PaU&z{vQm zwdE9ZUc3Bz6cad&oF9L>+U8Do1kDH8cn;%NX;a|SU?O{|NSXu}TWSHqTLd(BO-xLl zS$uc0U+R#Z*8}qTpCjEg0o-OoZDY)^na9UTQ;%}tPe$-j6hhl#8>8!{r^hpPkB%zT zUI#1rcFH^a60YIIBto)~JKZIe{3=HkbE|k)A;^gdX4wZeeJR|rhghVzv3@(9EaE1Mmjc4!?M)k64a1yUpKs;t&4~)#LvvRbmb?$3uomxf$b2!dNbk{X= zHNH$UL6Z~R3%M8Zh05U)BF6m8IO2j~y}Wqr=X~5_m0R6%p`ITlNAS_jM;eUrY788b zo+HWe99?yNQE@+>8-FSe_LwdfQ@YxVqTv~@Fj3moUhBOA@CTfb49S-_cknc_wV1X> z5`J&Z0QdCqH^FLYq9X5suEO)W9)#7dsZsInCRiN&D%Da-vb^S(Ym7OLH=v{U^JQNM zBw&<~?>R-KKQkl6J}!J_g%n1)ZavU@+LA$HvGC>Q-KORxdjHlE{d=h6o$>vF$1TC& zlY5P{wp&ibaR~|E@uaI}svI&W9i{9Tdk%KFd z!>9yfcFnk43&gD#N>-Czc@iNX(?5Q!vA9srm*#OgXLna@LQ!^l(^{ zHwSNKzlX1D0P1=Hs4GhEq_3%dnBmK9{+n}0Ek|o9w_=0MH9J2W*;_nv(ep4*CMqRD zLU?FzMDo%0oGkQQ67o@9y=L`rT({^+T1K71Sx$oqM}}}DXkbsiLc5uw2AKY6P?Q1L z?P?Zi9RKW{!QobQkoUMi6W=XAQJdR-5`%0Fzw&kjx%lum`=+RT!wpPP&W*MVG%6Bededd-5NECvi{ChW{dTWhQ}!)$+yfHL z%JT>G^<$*bRXS$)PXrXIq(-A8atT`FlHi zoy5BwY94g7^xkMOLNcJP5N&pri%)tw5{*Q^%m{0b-b>djuwZ#IN2e?(24H5&E~VN*oBtQyV82)+k&gpgOX zH*Q>CQ)l6Eh_Y5HPBroEX{Ry)|cN`mc? ztc~S1rPJL#7(D1$U0vn<6kdCALBnS&svvU%1B(#%L)$Q_OYW*i#Pw|_%r?Pmi*>jd zl^u}v+Dp5(VtiF)fA&c}NcCOw1NH8f=>`3Oye-0TNjhA=6{31BhaoK0+o8G57DD(Z zen;1D8?Ll2MU%DS*fY(-9SJ18O}`9Kf@4vq71^5gCr65Jzy%DzRsA7s$6;moQ{MRQ z*;*ZhJEZ%u_QfD#2f=u!R<`rI$_eloGCE%g2CT#McOSST_%E9$#v>%IXUnddpAE0; z9sLTqfrG@~Z5II8JYK?UGG1EZxE)lxQn$ThWAZgLH*Fw8ySJ8uPzVRk0X) z5fs?zJ1(2W z^O_H$sI-L4!aTnlqGY6y$uq%MrDMy$VQOL`D=F!>vhJGLClz0TNCBy6)F@F<_*+@M zaSS;`EGR(^w;5fITq<8z2wj5zKdz0kbj?yY`vtndcm{rpb15~=`(7- zDa4R_Cg{>X@CDvmxBabaFCEhZ{Ogj`!PEB+*F?kEk?p%Qw)P$>b_8dH=Ey)@8Q~RU#C)P9{EaG-`2W@ zDxS690uLAU;BvrA20u4I z5bcm7^?HR)I^1X^8`V!r@i!MXlSYOt7W~$(2&=d~>;uu3{8QO)$c=j)A@ydEOhK;q z+|nrP`%C=GiLbhRWcHV>lAMpG^IDF*%avHha2O%I{zIo3FJXZhi7$K9`|&p+OR@Kj zCt)8yKR6Se{=SF^C`u@Nd@fbY?xg9Fwl(Zsx3#If49sD%2+gJ1;5aHJG9m#p=PF#Y;2h&MHAQt z-5BQ8m41WOroKgy-LuPU1voV0`~0U(+5OE(RCL{T-`D(A@g z<6WaS-gjYhnVuo?1=__OY{^FZP*vLBs5XgRWVi9+(p2&Kro>{{GNX@iTE3mj7E|a} zftA9#2!Y1nB@2W2t-6n@ ztyre5Rg2~`*pDWrxMczh)3m5%&cgv8XEMC>u+#OS9cRCN467eP++(B%yzIBDp`E1Y zww77-ouk&(?qo9t6hcR~WBgWbgozmrGK|r9^nO-7O=8Yjb-pAvGJ>dzyU>1x$Is9o zTtdEme@vx9`<3=Bh4GJ4&?~`!(JtSsDqO-Ut8$=-MeF` zjMln_q{8VQGX_l}Rw-^3F4j4UX>wJzU0q2IHYxSnF-f}}u8=Rv&0iOLC#-bu82XejBm+te z+E6vd!kvEA1=acO_}>`}y`CCp4fV{Vfc0Fw-aRqV4k<<`-h%W_GfyK ze!0F&k<8nHfYlrNoVHiuAOC?hzX1pEH$$xNM*#=ls-n#<@6?IW?=`jOS59q;fx*KZ z9L)MlvCE=@jA9&;6e&$+WmdzL)A>`#NkaRtzH|QINSJ})-S&aPQu`(Q4$pMl>CTJV z8#e&K_*Mco)YrcEaR|Px4o|g*hH27TiZvTWZw3qoW?{+pDsB$bohD~@7>ItmbuKg} zLGuC)?5#>nCrK=t1yN9|#vKGsc2PlxOP+Un&Kv)8neb!ViG^I7ajD@i&EIDu(Nh1Z z)JE~t^-}chg59MsvybLsZjU<}LVn{2@B_Ej*X3+-=xXuI{sh*0q;1A9=5?>zj4PT4 zxt#%eA^kPqLw2t>=6V>=yD$Y%g*LCy{oVF0wdfUF?bZdX6E^zqXG)$YZvj ztVPw%$6P=HM6{Uj)=L+2A}XszoGQJyYP@8qX+B@FF;HMnk52<;oR~IT0eKUUJie2M zH&f4-5a}ZOYwIO&7)2^|fLxpb_3DmD_5#V$m}p4@zA}*k<&frl*P=gYYIa!}kpaSqcE9^qEeO48gBO7n z)5(if>DtxuO7}uOL_yckM&xPf-;tk$)JxG9(y;sFu$MEneJJP;*6|z6$HYRW3RN6$ zlSvADL{Jt>qs0F{(J9E8`E-iR!Hf`j^ETDP?~4jy`B=!${hf?b^xZ|{!Uk1v;A zrvA=%f)y|j1U^+c62pzW)FqGp3Xlv~MU~T?Z|qs`PkfuOpk@W>3y4SFuDSdsvM?pt zSeNDYNhyLL@6$b*t(y&zDTHO2fG~w4lAUJchllEo-5iNea(VAd0{(Iz86l7Bzu&2DDL9D4^2S}cLHn&Yw8gm{mA-`e_ zVop5E5~GxRBs{lZkA-v=EgrO%=~28>E(r3?fR4&c>R-~Eo0aZ;t^SSi7`R$K{wy(e z+lM$Q5T_4rdbH+SPG9hU1!mHKa40bqNr(HlKkT36h!>(2=RNe>#X8b_uPT{Tz0~sw z-36|SLPg&}!hUb`&mUh%%S!axi@;w<@k>MyryD;WKi`zJZ^qr(w8utd( zr_4R3L{Pk^#%RPwT7Q4TE&NnsxFo<{hu+ZJ@JqWG&z`~_wK3#jyUnE<|-8})Lj2fqHS{9(oDF&6Ui9`$Qz zIj~g@QW;SQN*>r?_Y+H(J-JqBrykAS<-n@qfOe{a3L0AA9`0 z!>^KP&%LnsGM4FTD2TFED2li(+1TUFsp7E>vOSW z_9I&^N0YVhcqdj|Y(Fb*SNx|7J1@I}x67sc=1glFV!DJ{c=b~dO>E})xISFK54{~$ zU_0GB``<$LMgCp`(Zpp#mffu=8C$lZSn7?0j{QI!KujmiuLsK+4Qn&zmJi4t-sCyh zVtsFw@jj~-3;EtSL-@yF>MT|L(fop}ib_SC^(@0~^I3^no-*h}Uv#zUP77;u1cKfJ zjEuqAZ7`sqq?A|Jb4QjMZ-%lo}Z1w z?Y)?&w5LbvSDFKNiWqi6UIAuQF0eH+W9APY5#}SZn+ogqdAzJ7s$FBt5#3Ge_VV)8 zm)^OYmAD5^*W$3gA4&;zr6l+?4A(BB)#x{kDT!WuCaQ1(4Gi?fQ0~^f zs;H<~?Q9k21m12n>GJ7!_H2%UP^#`cr)(7Ai5;`$l2s;Yy00a#7e#Uh?+%0-I;K_SZ7c!J}(>n6J!zbIC-!^6wh zJK{C2(u@e+zVwj(3|)%>l%OFL$oU14cyIqOS~WJEl5{AJ;BUj%>6 z8ygchf7gbcm7)h0d3I6f?Jg5Jx13Y+&xz7wo~rAz#=k>&!Lf5zvTmsx(1lytrC>61 zQ|vx6CkxNK-)QsLoUGAk!4$T8)$SFOl*FhcejZn3&^Uy+a>_|#Rpo4vm?*x~xjNkN zJUJpyG|0h(5H}As+Me#b8eI_{9pX0a^;r5Q1Il$rmskGxU(-ah60gfYizfo!l_38R zQ2t*L;;*VVR0Sf>G?L( zM!N#q6%nu_p zgE_^mNA#t_ve*~Ly<0&VHI1_+nT`E-1L^ulZ>+G25L{6b055Zuk$te zSb3#~KWO#mN5`qFEm9J$B3swFD;nG?g{!pMQFnhZn1X%1Ha-J-&|N1bDi7<4@}R9> za62wZu}cgfA<++wq`iO9*dvAyu&Hel$ zwX%gUqd0-JtUbvW1P>HiE28CuBq?j@HR5YL#&Z<^dmkTR{)q@eCJyO8skUkoZx0)y znXFZxfEhR|o!-rH7s+Hc*YwDf27A@Duk-6TfHtAStb>AoUZpwx60)p(P*=%;tqSex9GHhW;wVu zuBBdKFwphopeg+6rsyc6l|;m$>N_6lm!pMS#onh*f*F|h(E9MYcSS4dlg(0+jVs&6 zkW_6)AjJ7QuK68gmmOVm2gw@n}q8LkNZ@xT?%Sn;l+Zt_Y(=t z0{26%o4O7v>ioet@Wg(y6AoU`fk+Q6d=&RbCsUVPJ>HMvK zjwwb1@#;RWK_@0&aZuN+*jtqYl~*{Zgo`yVSpTCLcz-Ra-ZSO%w3&scm2=$F`M!%A zDcy4O!L~bmXLqLWp-PFfyF2uy-R#@B5ylG@pda>C`W1LiCL5g61t#vJ~bA}vm2e7O67hsSi4%CNa{X!-rmm< zt*R(KC?I}v>|wl{CiR1R5C_e;sV0kQO6#UE+F6j;o%ZpwPVq>?NNd`Ls`Z8SxEU7NXHm!1u*u9 zou_!y*|hLiPYSemUyg(JFK$g;g$Uk^&NrMN;J=H9Ock4J2VY*!uJ<_ zTC(+1W}%i!!zByYA5>14A9YoxU!8jaWurbmua7kWxdT#maJ}^Hx~}#U^`L#}0V<=} z#+^-GmFlC6Ms`SUw}lr2Gq0p2!FXTGs)syWM|ik$3%b|3aO5uM;SWx_tX#R2&*dlUw29i-=6O55^&8+WM*b|?BDfat*>`?e2Kjl0xybv zLsElq(z7FX{H{DzZIpYsJvxIsGv|G2EvTo?=ygug6J9%KJ?4tAvrRx}PcGfvOg&OQ zo*l0v#LtF}D%{(pOFe#PR8=xRk5zv~ z{PD&slui;*mt&8j$O+UR@Z&_Ykb*t=*nFyW5zGzFWzSI>{>grFcIAdhSl5EP@Ga!W zOUtT5f$UnUX|*p2&n^eXs)5WE&+Klr_d3}dRijwX!-Lu|-z1JAxdnLuEo0v9O!(N` zzZ@SyuDF}M*6~iJnl74#2Q9XRz=TxuY zgbEBBTW2+QjnTPPTfC1_P+jbh`}k0wt9zz}(l*YoZjzVVLQ0!f_Sp51f#XNj`uN7@0X7}1e zyP92L4N@%lRL?|n3Ah@ZqdnFJd56U9*S#uGDIon5dvxWCt42qn$Toetv8{2_0@n{`0)3Ib*;VJS|@3}%e|HJW_= z3G0KzOISd^(XX_;Rb1gvr9ftEz_x$vu3d=&?1}4EtXYAON|-9Db&}OX>`c0EoXz$q z2%%SteMkXRs5;+1Rd&hk{IbQsL56rge*sCK*KBcbRpYyGDR$%dgdnL>?eA37womI| z>grEOYsTjC7xDd zq|1`LmCc6*y>`-&vbjmKCygNoD2TmNymiaETr1q@=mifp()p*&_)RfW*RaS~wrVWP zo!g*n#AlkA33i2uZRHcOx>+l2P1oTPEp{8BZXJzOXXc)mrbSRTs*WZ`@LBi6lA8_KgZ5*07H;#t#+C55$}jsi|jjUW>GdAeyXTG1)SBN4UQ*{l{+bH!gGq-wsCJ= z>ooe{+qHT&3-18%m}5GOS>#rV+nO7%SgNI^>~E_ft&)4K6f>GG)@?rA)mj`2wX-{z z5>tJ$)hxTlxKB5h(zxwbtcGN4cyn7a*=`Fhjyd)kpIDyrdadV}Rg zT}3Vv{+y?aosMg+Loo#N?nOo=K5i+4515`ZC>-$+~6g*vtF1Fi-#*R zq26^C@h1e^Vj!%+4L1_PZTRU4{vnH{^wau0&fyC2d!V>WJN&t*smX+|+?VE89kbq% zo7!gHoH^b!HeO-kyUTsQ_#eth(Z*C(b24*n6V~IN zk2LWe4SZq8$v!+PuCP0^O*e59CGPkn5ddap2^Lsv8D)6HD9Ntbs8&$J#|fX+8CK*} z_2_G2mPWLAJH~Hc4xEH|=g)Dk#EGZc&epDtaK1!gSGZ&;_{CY}df-k^SyZJiX!wJN zXqWL%?2mWV4NJ@j+}9#~qD#g2uLEDac@uK6^KC&6qSGL<_mf_wqTJ+s^nl|t%euaR zI=CQdcWv&ZtURm8jK1VzaGcBe^qKqapzF$b;uX^UlNSJ(86cqDMO`^n9U7VPAAR8e zu-UR~-%+#b${{ZPdYBqHRP?2Wx@}bFLX#~8cQcjw3zeo#y$qajRG~s`jRvfs-ar;; z$ZTF|iI*7-mjKld-=$IWwu;Fh#A zXQufDstn!BAlNI;M1++bBKi@N`0TM)8Vp>ekY*zAZoyWvv3s5K*`%4i*9YR$&45=x zTL(L^oz(C8v~!iq^g>I!*0lIy^^p#cR9>(eOfwrrgtdgj4zr5Pcl_w}LeLt~gJUY+bRD_%;Pmyh=q} zAZ~n$Gt6$ix32$oIg^%0pdpM)#rlOq#O(?&R~luN>z4~&;v;$6L;K)~N>Y9ea# ziye_*Xa;mndSD6(WMA8R_X=DgXYiDrRK`!AWGgJ%rM0VHz%1bmnTizb0YRj)MyyAuAP)J?SeHXv0x zWwgVe3+6U`!XNWR{b&2>uTl?I2kO>kPH3~dCA^AS?-Q64Xh4X)U`JNI8ZODMUlKf+ z9&#NsJH4))SS|<#qOrYKy{E&j%4oF&!P0%T3MCtba0MF;5S6{CBEek=f?jdNSy>DVqox$>6OZts5Az1 zvxRb}_x-o01s&9+CUenEDFV*Mmkq0~-0=;ra9j6dpQ}gNx;05d9yIboRw44ur*Ws=!gPH@X6pbYZ=;=QVfWHHQM0sO2Lip6Nt{8o)G@2IUP}jQ%Bj|X z98;6natH0w;yH%TBk~xX6X!0X9UA+`HGN0a^-D)leZvM}QPR9AD@&{keEJ}`XY;L@ zCk7_dM^uu+jS;EbGe8(mTBi7+&-jEQTE&qxoEK(_I z`TQupWeZ_H{_bej^hjrQ7yP?Ni*ZNGMulWN zFP{RVNr{TUbKk9H05N{LJPh9ntC&o*FjuE;OqNDRA0j?k$haoO7 zk7z=?1X>?DTQ#K)buxqRH9hJyw(!S8(&iNHqq zlZw3UKkVsQwPMZWO!U>v@B*C?%I9Xai5bw!?!WpfVNu@Sl~- zUrKF>Ld=PAyKR9&DE?j2Ap!k;nYDVdN1AmYt~9uu>RNTQ%tu%XH3Z-jD^HJmtM??Ld!ys8T+>^oO67S26NX_28W6gEB-!TlAt@s z@*K1zD;Ce#wMx=bdfR+x)9aLy1y=a5ZgHHchCaAA*@HKWkwFi?LUQU$)Ad#-?x_ze zl70Tcu5b&a!XWzO*fDXpKJz9RT@Yp4S#u|pA2qE_*Xj-{);6MRPj94b%slL?8lSw?0Ej;h=TFC{v2OT14f_jt{TtOJd@(0hjVQM$vQMWJRO)l|0K>e=mVH^vNNUnZ zTQNc0!v>ar@^h_n6<<13Q&uF3y_1O*J8S@wZMRAVNW~GiEVgo z@b@_gdZ-N1XiSyVR?$Q@Q0R|48R0=o~R`nuW(TeNEM%EZg7jEY>Gjc~5tg_?U`c z$3`5O-5gVLGu3_utVt%Z1syQ(?;B2O;oN%y`iDgVF|SNNS8BY~_f)PK0NIFA|8mGU zVqjo={RSM;sP}}xURA&n;8bLOj2(Je4vZV0(xg7LrjTrWNafss9*#~bvY$ zrfnWraQ?lSKo!qm-0^BwJwN6cb@0H)YZ;w!_OLqI0O&YRCOTH@ zZu)~GC{=_WWCK$B>TLMFN*kD}Ise?`1HRsn3BzhQBK@LbbYn#0?^6IF%{GcE6skkK zfwc*srF3#zC&oZoZp-fNAdpk}^9D8z&DD^amKrJirn+%wah#$1bmr?tds+M=tibFO zn0b=wN~33>P%-pqn2#8kbN5?=`6wllL;r1}|Lc*y@~v) z3Uuh5Bjk!|fB;++A##bPAy_|d1C}R$n*8a1E&A_YC9u#Qd;`jI53mJKdOAWH;NL}u zoP;*u@7QY{px~O13&`ulb@M$b4qgCdG z1fE70MPrXOspjbM5YoT`or+s?2b8rFPlXaf;?UU?*gQkX+*~n9J~!*5SlDFT7oEflFbx)C0pqQ zvjrQ%Ht+wv;RAE#pLSh>qV0MBKUBp)kKCH1>i}RuFob7EDH79}!!4>Fg!!VQz^8x! zTmXwhf89_P{e7rP-&X$YG%@+=o&0ZN|FFX8{bBLR^!%0rwK5L1#fH}t%Z0W>uk?Yl ztaSU5c63X%>1qoX7~*+bO8iZ7(jaz_(-o+FSUZBP6UD*zIfM{jL& zG56rzgEHOWoxQU=ccB2im`Iwj051_(!Cn@4GZY|gUjqz-%BfW|(!SxMM`+VmfITk+ z&UW*^==|S^=ZA|P?cWem@5%zc>oe7H9+F40F^`&p0v#bbB7Awx(S42jiro*kzWoXM ztMhPS!2GO**8bk2et)~4Jsyx7fMCDM6oD0Ha0e5>Yxczp< z!XNxrn1=^VN3ySx*bmlYPIYKni5!S+*n%9XBX-L4`M~pKDPha2L3}+%h_(9CO={CB zZU_5+S_a^Zm;wvDyOrDU8DPVbRZ7vuQ!|;9#kar6L`SuCJhxlhdd=2(R&mLFMkP|q z>OsiEHNjY@7>9P90t;!v7Kra3H2@IT9s|(#cw3Dk0C3~?_QRQV{Q&;D*_%@Udj0tT z!(dU!YQd7lD(g$hH^?*sh%{ukf$`rzDd0OB;XTPl0|H8pOD?YI@q-|Z(gc9gR)Zi% z47NKRtmJmn+NHL*pK+b6N@{C6nv4Lf&XR;o2<|DlKNv&R!RR4?dmk{8 zSb#11X_4SE80OmaX(aZyaP}Lwoq6eIKQ@N55Rm;th6tSOI|KRvX1S@1^o?uPh23OL zmpRtO1(tB`i4@q*s0>Y#TZjYXRF1QOQ>kZ{!zanJ!NGH>pHmi;`Q}|`+i?eTZf~4^ z^GXH5LZUj35Wn*T#d_O-e7)%;u3)CC-N-v(WNS^+iv-%IvAOWlI~E%|=R}z+;y45e zA;}a;hg)AKl1UDMVo;J;MFJPVzz>A6U(x4zjSGYtIRK1eG~<>_^l^R^gs^4L0G&_iDx7!TD#ijwTAu$pF>k341lT zDoGIfXf;dsS^B4p?1bqzueLZWho39zR}F*g*lrQY4WCv z`%-AbGcVg@0Vw23hChp` zKj`@A+%>c4siMZfdasjF{43>-+4yX!KVIQk^1C7Rnsuu`?z4ELaUpRi z%D6>4F#ZvDZ(zO!*jPgz%I}|>p8!wH;HtLsyV#G7yqTsxtjkfyEl#8Sv^J8Q{@zx~ z{(hub6eoK>{OUF68ro1&Yhl;=Xr=()weEYjlA!uw*hfb7psZ)yq2x#*w%!0Rnp6*u z&YT0FWfT)qQ#(>AjcuHf2%0;aj_}>7o{!5<$fUOSHb7XUci)!19TNk6-ymH#)bZ_~ zckC;Lm3qHwR4z1asxeYec~}XLi;IDk@zt@qW`9k4(CWfWZSM~+kL+G9({3}9-YW0Q z@XCXUBCx>`SruhGT%BVJuEef8&2Zv%l#TL+LFMk?^wO#C>{oEPJDK&Hr3j7j=9?*< zH7sQ`RoR55&Nc^Kc)pH);M^M}H}7H4XBFvAO22+6sQ4r=TR}-#F8@hhwLWx8xB z2N!$)bTk*r3>^LGOf>AuGcN%$m)Z3w3b~t`&Yaok5)bP??uU{3{5%S#S!N_tOuSn? zfFwpt^wGjgPa2q_h`Dc(_Sq>>(X);}Z2r=<0j@4J8%`-YpHI@&Q7!23YQ!oj=?1R$ z6u4W=t{qHhbmV&!uM0@i2>@vS?QD++uns~Z38t?I!HqNZF z-dJy$>{0}|G>@sn8+P%kXfVOHT;P8OWvZt?12drdI)|-FP-Cpoi+dxi*)yxPteT&{ zo0^?$(nrdRbh;U%F^v{bNn=??>ny)*DwUOaRQa9j)s_XP^~hX>7&JEfSkJRLiF|jv z=C!WVQ|B4OUGS>%s2w2ZpTC?;Ds5&#MNsobjH(51mpo@8uj~g}8I5n zbSQ*q5``i+lTFjPKMp@m!lRh4b52s57Qn%5;N z+$&xcu91bWTT3CblO`-LbG!E!c@28~!zsPyg$=?h@!3k4ITOigo7NA~CKU09`l8o| zL{u#)DOZ+U7ovMbA>@@SL{$eG9-dd2%PRm4{#Q#6`3#WSR9+JP00MUKD-1+M`EU9= zjAALL?oUd)*X%?^$qpsEFmF8X4`ujIu^A0f-SGi;9R1m?mUQa>Phd?&3w+M*V`P*; z-qd1&`swz!(w3M`)N#-Aqp6=gCH0OFn7-quWD`X@ehyvBWD}12l8yIp2;Q^~$mgrS zYle-;7E=$S=G7$S}Hy3jPKalt5|{xvY2YIjlIfv0=3c(g)0 zhsKk3^H$$qKie~=mvHSx?e>1`3n-E6LcFeaM*8>WhT?~TN?$nG`A~)+qLZKOLd6H` z%(_s!{mCIWbJq2~)#{K8jB@Ys@)0yRC^hNC9=0EFKEw3PEyz}l^b%Ipe`a#5A$Jm? zVKY7x;$@Cz|J18&6;es1IW=JECcF8$)xuP&6vp|0C2$x^V6#-F37}F)z+hm zlFZ7W9w*C9Ef_#(|4|-PL;HM=azoMxlxcj-9dvjxuKGujPi;Oj`=6JS{!f1EYlHoj z*_;e_=_=WXYgPR~-V_cz@3_}tZ@+es(nQInTJt%CT$P&d;I7tY%43o$)S}03AB3c) z(l&HRRPz?NH|Bb(N|r{ixeqcWkmj>xVYxY`zrU9@=XjKx{%~D7PkF}4j0bir7q)4Y zmV&8o#)^uL5 zV4t?`mCJEntAkg|m!UJXF|JD^zN$9mbr|fq3w534#sy3dCNn(L8ctk7SmpHn=?@&h zw*~!5#Hr|R4((DZx61u-xdUU)s<~G)0|tpi2z-X$1xXsULB!Ua{oCLLRb#&6&{2^-P1hzH0y1 zt!L@$+b$(sA_jI8f;!&di#^Ag5R!^A=M%Xj12R>nzEqrb+#OUW(kJ2!ZGn`~E{)^0y_ujFte{5Qkiunw}T$ zf}HQGPi|XRUu>M<+>KhUtjxB0Q_!LPCgfwroF)!_kCK?@!Vj+Kuc>-xt(8N3z|cPb zd4lv&gDmv5%H@EZ>~juiR)Sx#h555%rJecq&*kjugZk^!1!`J0?o(oCZYMNGNmEi{ zram5sM%8RHod$XDRT5rdy>P@xMjsE!{h!|iEvIJPEvNLFq51y7D7OBshLAHKsgFI? z?J^+HIqV(m%U@rgHuV2J}QEa z1CK;Ai3hlsnu_Xj#fqEbtZdxVmo7gZkS~ZDT$Q_54xDYtoeW>Y%N8BgN0j%-s)Rw+ zyS=K4+&gfL`kXaC2k~92^;v!cS5CizQbf{>x0=>dRW+YbM@RWcDs}F;x!Nr`cXt%u z$=Rc+`u!hMW~`ZB`v;NYj_D_}7IsN#uo|=LjuOzSPc9FqkH_MSU0*^0OM0Y%`1`nv z^`GBZf?egY_Xp%~Q4qrr9j@cs)yCV;NnU$8>Y^ne)tn-`9Eyp42VBM2Vc+;M$FT zk*bi#ejwGH&71!#7w@W`5AJ=y3_BffIJ028{;OH>saDCC0fLr@cz zecaZ2^+3FFO0*JW`!*tuW7bx$fV0FIpc`va*Ny6$!HFwwsS^Hwl4K~I;Yr4vImz(( z%%)jzTK)F2Xya;<0_Cb)HBTP?OnnWiTPf-QMcMTlxqN^=utOh)#v8Wt7e$lG#h-RAPAYzFm!YprZ<2=4XS#Ukl?b7p}XB20c!? zZ>HIVqPS+*I$-y_xVcus4{|6pOAXGGp5II_)m;yxRlu#P zf45lv+b6$YwLv(6fda=nqdE3|xD~eY-o}YxR+S+Ni$m`F%-O{e|XqAC9+}-%`YOrmB z&LE!l*$_crUkq@gy<*+G+PY9^y9HjI8Q{a2>YhL(-E>{3zTcX{Po8*{?eEoH=({jg z+MIJy0IH=drDh*hdo7HA7-$0_pPAnzFYroP^^Tmn0qWdX^%EEINnXh#T%?`wWb~P< z8*%B~EljK4{px*rq0c$c_kdb@jC;Ph%p}xdq2MO!^48@A>4P?GfTM@Galdv_3+T6n z<3#4r0~fV%2`fUw9dZ)?h4KI0i;s!QJcbov8aZa{x?UN29GAaDIZrR*c-(g{4#K0R z3bV7htU9?)M3K;uoxoHP_r;>hu=U*5g9K|0xiE-r;$6158AU3iR7=wdd5A>FDkssMn1)>M5EoPbJg@N z0A1?9VNe#UzJ8M~oOxSn=Fzzx{G90QJ!ouBnz4}63SBp$=-Wxe4$SBaL%15gQJ^w^k>3|K z|Gp8>HAYd~V?_|~v>pYuHLc@)q07hGJ! znzinA=kJc?N5(&!Iww6o1j>Z_I5U~+7Ih_6^7x7!R$AwJ_u$#~P8B1QrX+b4Fl(d7q+~pUOh8m+38}MBmKpkzmT`01 zRn5-BdM_c$(GpR{xn7Zb=nM}Zc}N%s&3&F8ezErX$de3}kNh=DDUeezZ+$i;`K$Oc zEKm5jL8<-SSgy_1SB0jEH12H0HjH(4tz8EYna5vUh-Cpkm#Ab;L2+v-%9~Yle!IJ* z|4H2b`P-*&DE2roMCaE)w^)e3oupK-dP(vM9mBJd^+ko7<0V;FJ3Em;Sxaz@^Q`kR zx`eV;PIc~Qd-+_AG($FiTA+rOtlpwG$p>aYvEHxy)MUo_g`z)X?TE??vt(9;EIl4k zt2wm0W1NOnqt`CE`Q_pzzfHtd$O^WKgsw|W%y3Th4MW453(&VS^YMI=w1`y`)YFMk zgr7N|QikD%Z@9JAmcg5-7CEm=i2X^I(uO-T6pEc66k5|uBEP{jQWf$K8cP~-g=&{W z0_O1NC>B%UC%}`Id|dyTxFlu;kE=?@@`;c70kO!(s#79qa(y!>@s<%I=wSDM$t?3&?o7N`AT=_}|E9`N4Vq3fwThgts}Nog570;}p=<+f^}M8gD)$}R!D z*xHDYnA>IPMs{C2Dwl44*B+~UNUl}&=W7e|u-g`ax?IvR*zssZO9!5rb27G1f(^`+LpKOy90!g3HkF;d^bMq;dLib*PYjs$ zVJcaNS#zrB8Ab=0ktf?V{MZyh*GvZmSFXMoJW%Ifd(WYfC9l(Wy=kV%reDTYD@6g} zmK61IJ?lbvf<@Mj zk&78I8G9EY7jt%j5AvTC@4$d5_P_C$)8xw*ib$hnl3V=v=)VD7F~t19VbX72A)bB# zm;jsNSu`X7bhKZ7KY#nxt1VQF5nd~qrR%@+g+L$w**2E6wvj27xPku8 z`;N$07LPrpvrf@pXm)h41O(T=8P_-s?8n~2+BpqmQC5_+&nCJY0rA|j`a+0F3N4@r zEjIRTCXl80d`ScW;@_L@4TJfQumm z4*N`Q3>0@^<9_`Sb-_X&zk#DnSZPg|Mwh?)hnfG6YwV$$Vl)@;W;tl` zCLGa7&;uQ`Z&%6V>poqGcSI2Ec%~tQz$NeNf1?IiyTY?o&X7yXgP<%Jt>lAw0z%0m zFYKB__SJORv9oD2|Hqe{$wm(iAYop>onxQ^v)9>}e^{S#R3W&z2-_J2op{#c^)m@EW3zS;MRl)wAd|^#dW|Wp&sST#7 zj0Y5}1k{><$w%Y6g-AyJzts#N+Rk-=vBwuArtBcW_J1o99ZTD)bNd(rop5joJ3vI0 z6i&4WON)xuEIFB~M;$F6MD~`sxi3`<4TFXpfsJlGo8n46PESXIub8MPxPSd!!C1at zJ^yA!zdEX;vqTHbff&l&i2Cv%b=5e^u(V`koJo>PWXDq-bZAwRU2zHDE{C7q%;>gbRy-6 z&3_-fAadJ^#c4dEC08|Gn`6J7{s~(`ygBO#gM5ec1JlTn&e^8>i)g{(vr@>UPL_@* z{e=bIzOg=j!pN|IYle^b&AK~_$XD7OD?v;31L64(zu|Zh9%QypG?GtOi&Cps2G!b> z;86`%-ODxS3Bp}Cq|5f(h#SCslM<0R>$oc!G}%ZE&}xy_iWd?*w^QLDp`ucHG+D&@ zHH{)7yj)yTDS%wv6fobHf?HD5!EQW>#Eq)GT zx*ztZC*#9uD4?sA4U8za2wtGAg0jn-bD;86N_LK2k!(NmZIiW{viH(aeTTA*v2-Z1 z8sF(&)y0KfuJN_JpLHNh+Sun%CKjOAgmqg`JnAXDgPcl=F-hSQr?C;}>>k7`_HAOf zwTT86z|rpY>gaGtbDR9hR@xJ7SCO?yNE*n*8wIJ2jBr2>?wXjIe;_(Jkw0rfrk#LT9B+ zjuDB#?(3Z9BBuX2qUIk@_g{LLvKmS860#L0Y<8Z{y4H zt4%zSuB+W|UAb7cSjy`l1?Gv)o}O$}yBvJneGfs3Z&qHs3F?-ij%`RkDQ6c4;fsf3 z?_PO{>`J3c1|67Mj2`a0wycIvz?kuTzH8_zl)jJS%IsS)DM4X5s}1~xu!0hjV)EiX zWp4H)tu+4&Z<{?{ohE2)ncKsrMhEl-d2W~4Ws6e{LlS_5pkte+SKd9`EtB4j6P(T4 zI;94a4q6rq=%ntSx(4^qh}y5isIuygq-sYjwRboKsYKVHv#v>)Tk`_cpbXrROZx(USvb_?ft<9f*>`xt?LwOU@+K&j}%st&U$xk5Uo zNmE0&lOJbK;mm@wBGiKY8-oNoVZBk1`eVS0q<>+)BQinG9P=U7l!!ZR(02LVB)!}O zE$Ms~zSn1iqKX~n2UK}ph%DmW=dVR~xw6PM_Ihg}$9@{{*o z?f0v~W=*BPcPiWK8x@0;2{F1+Enczhe7R?`YT2Y)`0N}cl<4$CUTw37PTOGl)O#H9 zy#_g>;dF0hQh&~-SuQi+NE8j0p0d-)f4xo2~};zenyTwv~<#V)x{x6e*XGxR1a(WFv-) zqT7uZGU{E8fr!5y>{CdQ1-&o4M#~k23TTV-S(LGmm4URfF_(p$WmXK?%_y8!0WpB0LhZ~j%e1@ekEaN@jk zRfYi*^)C5UUcQzzDA&%G1TBZbRAS@yH}YqI&W}vdgYS(KzKq6%p;;2xI;5tgwOQD% zf7mXzD{)!fGTk2z7{A;?PrrF;c=E)bg5zlzv|8+BzszWSs1mZyQkJO(w@cjnrwcq|5Yf#qPNSMvnzjVg}!CdBk|sVVK71%HI5`K;J$>pi6`xT68ujScX=b}`-cZv*2( z7k;GJQf+@RvgBuA5p?4_n4SSI=rm0WFdJ}#$RdKwSs+xPAwM^A)s9N*R@FRoM)laY zz|m`U8x+`e@EyU_fIq<}J$Qc(n~Yt?P;&#nrM;a4VLA@Wn;WS+$|L1<8Md2%uq|cN zCx*w%Di=*w{9C(~wnOYR!Nr@%BFGz{Jhq|X?}?$maOXk&oz8Q9+ip4T^cIXsE~%vc ztW)Bs{s9?#57)7*FCqLDf(tGlI+HDbcF4r(w(?JKp~jnfEXs;(E? zt_^z@8)0I4_ZERU8;n{M8JPmqRrYJaQe4qm8X7Jx9;8EgdE3f5URl`EElnNTMUy4U zmxp3ON0SP_Ssvzy+C!eRv1Gppn%#}BBZ+Cj=>=Cx{A!ci;_}+MWLBbt)G&*IuAbD*c5vAKPz4poG!Y z))a*4!YCm+f?egkkJD1cRYuc@>MW`^v3_gCaN#KKNH4hBsFbtwAacA@wGMM~|H(Ja zV$7GG^DxtBgtpEteyvAsj$L?3dLvJ}2n*I_faB9is_OLi6&w`=9<2?$_%d*90k~`* z`|ofWfC}8jLM18fgHnkRbx_HSK45-!ZFp%@7VH2<^X>#yAD#Z5{Mjq^e>0_KmA6zjHXVTVis4@SyazFBxB@vX>tf- z+)=7vofKJrInM8Pk$;Oz-fNyxyUjLk=yuUrQ&1yl3cAg4En!d&gqcGb)i?YvptxUo z`r7{Q+04Gb=x+b9qx_IpbwFiNxS%hqKX9zgi+1jM?_h}IpQ!OkC%v<1U_K$nk&qu6 zzhAl3qLS-4K~xI`USyHKm@II0KFY-c?2U$PyLKa3n4=3Cmk=B6@w=iJ-26xpv*-Nd z1e5NSVV`18oy{-bx4r(IYKkn*O#YJ`5pk?^6UgFoPGR$cp21s}B)M_(tIozWL!SRV z81A#3|F4URk?i=11%(W2w8F^l%hkuPxllWO8eB=k2SgN( z3fH7sY*+mkj{roVv2UVSi^7=gxBNIo->ot%H@V_$&)@Y#^Duc;6R#SkkT5VCQzd#L z>X@sM$gQ8IA<^B*CD-W!nr_>zQ&cgAr6?W6a%EyT>&NGRG;*%#{VrAq3{-Wc7TAkP_yQL%#W9m1 zB?Q7%YC9RMzpDiXgu_H(zF1lM#Xj!X!m)FU=@4UBBjNZtuk)ML?Q~EtwYzJm0nnGB zFx;W*A_;v+#E_4j;1>bb8CD{41EUyq0Y+4AmbJt@R|=sUy1ppjzJtC9Gxdd{*yqN9 zY7I%AEie#@vpH1z)-y);kGp=zcAK?(fAcX6fjKjyAZv*e6dF4 zLpj~ki|d~>DQWOBvBbQc6BUr4tp62@2b7+ul{$P9KFAS`=Y+a}v4#^ZzU0X}hHRjs zyqRyGyo4Ml8xsK##d$1J@V-exER|=F9OW||plWx>_|<_P`h`XPY5bXiAy;RLDF+2R z-bqsNxcNcPj?+@%#bEVsdd`KD$4&H z_UlYZuYxfyo!CkJ8~5;vSIWucZ@&@=L}{V2F_@0QOnri^4dMBXF8{%d(Ex@oY7v3t zM*?>RqgfB&*3~}%3mc0c*Q(`Jef#_AXcZ9;Zzf;Q%xN{PfT&@x%*)GfE^<6b6p%@j zV3{VkO)|7HQMH1nh4zr`*=&J;%g=ErZ%SjgJ6x*D{1MuxtY0Zd|G{4$5~xQsacs>D{h^;F`2oUCk|Zaxg?B!zYE^> zkUk{>f)8Rg+dJ*(C`ga)y9LjZt0q!;^yD! z7P5_b|JUerPLLRXfXDUUT1g~j(>X8V!I!h^TbWqA?F~E%c))h>BY51N)VC7K>M+%` zIbE4&N6vZty}W>ri0EJi8V?OIi@2Z?Kc$Q?E_<=*ad|Ld`F#I<0i}dNBvC7;^y=IL z$^{fzy(ojv*&Eu-RblpRn>VGaO&H&fYHVpZy9*IQqUyv6_rqr4`|_3K85zgITKbtjphSR1s1^(&Gj zH~A^c;=zftYBHEnxTtJNwSH`_|5Lta%zNh>R(xBXo7#uaeq zlx)@~$sQ$4G1UprqL9-f&T+7WI8=$^th7Ofx@>A#s8tgquu>j?DG71aeyeZKxS;<% z_%v7F4P|-^Z+qqE1}Q)m>ALTxs?oL+uQT_S6l;WaJ)M^2^f({e^hbl`5QDmNq2Xdx zo7GQ2@dOh_v7AXrPiOp1mG+fDxrKm>=OmFTl0+qQ!J*(8GkrNGkO3k@lPE5F*OGZY z($FGF<87F(ehd60W@dbJ!TpZ=PUMV!!!Y6c(!)L?>GeW#tEltImPo<`a=9`>#=L4k z>4nB#(0FG~xW>eNO!$U=tm*XL_GqLs z=UyWMd2fT1H_+Xyv3?S`@^9g4rw%y(USg2z3{l^}aCgB%#fSgd9f7tRbppzsTonyZ zn_Gd3KdGB%PYrXt@%~(A5h24GW81Ya+wMz^Q*SOwSX2Po@L>xZ#YNQ2+h-DiV2P@I z>J*N+(XMwk>-zU%k7&Ao&K+%giy=lyk(6yyw$^-Me5X2q=kC z5h^uISuq>}Dx=&*`XUS?+03fB^UO@jnBof>&M)%+fuB*;(~4V+7rL%8&O;>9wwlk& zKvhU7oAB6}qw8Ur@G`nC@+lGgg^*p1;9N)R|A4&o0n$M%(ZpED6BUq1Qb2)oHn+Sh zV4mpnO=S(6Fc6%$OLaaj5~;ZoXsnvY9G*Q!4WqHurT=ernKlsbtf4Sz6-zvLFmJ&P z6pW|1ls%m&x3kWp3XyE_RTC)r_7=={B6?Tu`k91b7BxACuB*eC+l-htQg8m=Oa7r* z5>${|83_BRi7_ja0FjD*Z-3IBudD54$LDtSd{hCdpuZB3xn$zO2ORepU~z(s#o40|xqOHzRO->DaTg65M4A+(7E>b$G zrqY*Y)IFU^&(3GsZIJ885pyIG12y)EjRdrNW6Hz`XFN0bAy;Ty5$D{}|0K7<7uFs= zn`Ba_J>#2@PbVDadu&TfYeAhvLoHNT6hYkDZp^)DZl@P*`rjnY1@4F%d?gp1-7o2C zi&D-?(Q8yJbljT%ILt=ZjTa9jJ9u6Xdx@!AZtA^XMp z&6GU3(A3n_LArh0+8tg(0+7#`*jG4ySlV zoa>+lpd~u;xFajscD&LfIL@^*ve$Lk*fU9%3mr%ldl38JuBMxq!hjWWy7=)9T<*+NeFH6i_XF>WIdIrP7h z+bpIK#~#I>9ET*{BOn;pJV)iKuUjo*TMwf1kNRycf(J{iE$-_o#h%rl^NJ5y@u{ePs? zHYs0x85; zEg&G8s70>Mp2QdP-615|V2^FYp$@8_cf1qIY-JS`-%+I)CQ$g5@&3x!AdX`n>9M7B z>dK3k&|6n4CauFO)-riFWtvUb5$|WE6HH`^WFojGvXq8;E!BjQ_Ke=up#61D3%INV zw!>#p{-%+(hvn7{UvC$<22tBr!QJgU_p)_ZPaQ`CuHel*B76BNhX2^bAu=!CHpm=S z-lH)gY=h{q)$dNYZ!;=e_BBMF2_fA0(*M8ob7qk-TXl)?bErfy$+k0Qc`hlCD{4Z_#s61$qZaFd{c%%fjJgU^0T9wdT8S4@dh~ zFOkD`YYts=U)9Jz4MZqe0fWw^(0p0BpQu$X{iSQ%F~i<~lY(CrD#1}jZ6&9Pv4I>NlC!M^(IK*sA>oS&S`RF$=bO^|upsRs*sVu!NE zJEyKLQO_=Im1&=9<-Pi%p<&=@tIDs{Ea))7Tvv5^f4u_7#ke}grPXMBw%V+gT~XV0 z$zYxL(IyS9OtHqF;v~yRnR7BLxZ39YeNoh4*-^(De}2#b(h`-%HygU9nrzU(cGG$G zFeM{H@}4D(T2;p>rFv&M3ymiF#j4kAUwrg*r}J85$!33RXkkD|v4ioUu>*AXbW^Be zccJdi%WL@ic*gO(a*y6yY%n}bj1{jxr!)IXBo-6!fYG%uh&7HtxvY>atWH9@EkCi_ z&2iPbV!^*EC9SGzkvG;mGn-Iv;T)6XTJ|x_9M#o>~g|8!@;EY;r+JB zy3-3I4yVWZ!VBwuLz966omv|cFOMOox8E-){lL;<20xhozP#ovUbv0);teA1A(VJ* z?f~}^nNr{i* z{s{jkdt`Qy!3D0I4=AKeLu$q1=aU(PNMBXQ)Jr5D1L=2DuKk%eKnpfMp+ud zC5~4SzW@JmPZ*Ss^N_r2)}~;IYKpY)#1?<~{NFP(e>(qvv%%v6un~g3jRzo~^UwXn zso#YbY_qKVQDprCMoX&3=r8}&upe?osvH2z9wxNsqA&6aexf^fSW&o`en6Evk00Q{ zukf5NvT~2Pat1jW0>~+Q34Y#!~|0*k0PCSYb{(W_Z{fXIO+J`FTKM zfP;;Jh&_BX6~g!^QLA<|9Cj-(LxiozvauR_$?oK@^BoRAd0>%sLf~{w!lmK3UzHM| z^E@}%OB;P9&#x(Gksyc*H``A+NImn4-<263=q_muDzXKf6I-SXK#Ej>@?;LaxBa?0 z8yGu9kM?)$C{gi9`+SKkjD9b`Ifx#)(6C^!Dyg}bpef}B#?LTt>yH(e0Fpurg9NvJ zlIXvLXGExwh|IdmKctocL~BpS{B3Rj4*(|+DG)bb<72y5{{s^Y%=p`rqn#5qyMO{= z0jegJo3c?nK?RkzGhXTOhqoa_H)J@^pQ8DJyopy_PiIFaNwV+X`F)|<-yq{FF7OZ8 z3Wc{(iB*3ps7s4~8kKLdoCQw|av*q`$btnTsCsg5MaN1}JyZSfU^9`VvwI2q=U&9X zy~spho-hBv#RN!tjobfiVvw=X#UN$M?!}+DCi#~?Mb82q0EITkKmW9Ak$>Bh&Ofs! z$V;x(hyW5<##$npu>8|#qgjKPX6o7b5N%?SGm+-_DbfRIKY`;vbv_er0Rz?Mz!?IV ze<#HrLrDwij{Fg_DME#G9@YncUQv4G&tQhg>-@<|pfdjjif)IVnSuwbe~R>jRh$R` zx)G-lP)POo5)$?UGSn}UaR=pm;2{u(9i)LbYKg)%=J^qAyv&>ULDzGG;J{qY-UpW? zP<$IAk`VAuaPViq{@tig*KDmEvIH!aGyGH#p6MWeajmCa3D12Qh=nSd>fmptD*Uk8 z2s*%Z$0cgKG_*arq|9LtFSw)pgpsYt9g4T^oDT_bzl;yOyUA;EKWy4#Wq0o_I)ZGs zZ?kAl1;21GPf_AW z5=Ok|SNX|zAGE42T;Ok!6uYnVpT6ZTG7YzHX>kT?_G7zeV*bpd{aF&sSnNzsEdSIK z%7A9Mrp4K3Z*%|FXIw`5zkKG!96mCMS~s6EfeQvht9}5_oImpR=XUIoIS^lPEJqW` zWp|OLzb*ftI#~abm6l)fI4h#gV>4I(xetjqNV}q zHgCK(koIst(^siPe_B@Y|L`0J$hi{AX+I9-$v$7agquzT*ljr_g@KXI+di~ng8`LT z<);D9aytY2^t{iVBIJx^)T>U$DXnc1iCS~^)p+in zH-3hQ%bouoB7Q1-MO!<*{mnYatOn9(S`hRiTsh{e&-Y&?rDh)>Iqa^UK8-}NZ}K z_#JCeNPPXOQ^R*dhf-RGm1oYUN!E7VPX}r*nGjl!wSGT)!u>y}hL_^6#P}H0te zRHm9H)6ds-39h(&>wX<8=J#5v_Fw}cXfsIVE6qYwBBEj~a}@1=bDgF#!tua(WzFRT zviwp76ROsTIT&C9>iLPfEjZUY6HInLo-8UHa%(ls3$bMJoK$A$MI~FFR!%o@`D3d! z_%NdX>`4;gaAyj*UdSW#RRbDaa$EY}Fp9@h&@o7^tfdD=v&BJhaR${9(;qx8qwxUi-8?D6RH=E_p$+J8t0@=?!2((7~o(}P|eaJrfKue5ennqjzoyj;KF3=d!AZblk3!dvL`QW+&bA(E@>i^%eiij0H_2gqI@G^T``+~8fro{j?y!2D znhv0uz|XLbGWHdD04bMd7m@uK{nMe;W<{qpi!ICg2FW40YH*j!q9R>h64WetIsHt& zW-4=`J}E_n-L46*chI7nDNdXs{u-zq3|A%{V!Xs%cWY)JiH6i!j4p7kth)?5{vs78 z#7cp7N9R8{$s#pa5g~o1bZ=*wjDRM-|VtvhN(2Yq2>v!Dpr*@g>IXwu8ZpK8OZB-Z^84`Z-XCnIqR?czFo zB^BVwP}sb)m`<}oorY?5p*=f)F6)OP;qD#t54U`eAxgqnoN`4u^UVGS)ESDzoPC$o z8re6tdsgMM9NC510JpJEl%t%uT5KKj$@Dj5?x25m^i2v=PcyApszmVg_{~rYFKu{X z-wVJWCpb7!E)uF`s~}e9gr2tO^5>|;l+S2eFljW`Hnb=YrEy>+6S5u3hsYowPv1^$lA2%%)D-5e`(t}aU2f&Ezx=$X%SyNQ`<&=cG%S-)5?X;|$ZzKMq zV=S9;RbtL6aO*>GZ07);hOWeqF7m^xi((un?vAX#Qg{e)`Cqw97}Cuw9c`|59NKDR zsCyB&oCMjR_@tWUSNKj(*I2{EL$-uP8+}G;Mx?(1L;6OcV`5+#k0C!7qSr)_H%Fyz zr#WlTi$j6V@7|&NU=qx_Sav#i^}PYC;g!_P8`}O7Z0<$|7p9;KWg91lSJq9rjRUty zc+y{xNv0P1%zQ97jDFi(8zsOqCCKke%>B|!=rnfgXh6b-sHWWaGTKUJQv4Y6z=F9& zmgH)$r7-NWqc47m)z&VzhDGv9|4G8@)8W?nw=W&$I_pfE3m|E{MOIvMgUJS*E*ssK z9+p~ig(Nzxjv?OQk1sv@T&Pq@0U0bkhBM#Rh^ce9JYEOiF2K*3bXU?N?J4xaq&nfz zh}YH%e3f&k5c0|*IIaULWL4UtB*6P7B7)k#c1LuV>7Dw+>HW%ec8Yd19ern#YpxLn z(^GwNhE79B*;zzoa`Bk`PB7`2pZTV>WcKOMU z3W`~Wg>Za)k_s$#Dg+woA?5}%d$Pw6jL~ovv-oERW&3Qy_Bx{qNuQx6T%B-P!J|=s zZ;LJHEnl9LgQy&*>&+4>JL%B-_{<&F@?5#}rY#42{&466C8&8bCQMncPS44|PEM_< zXYbx*ku)6N944siXqr#o&W&s$5UHYyWtGZYDic0AL zM8#SPVaDv`=Nd@uf32#z+0m{ytweG*9nGOU;GoMhx{Tw|0TO8wn(MOq-q*#e2*YvC zGaoBgAUx{I8{;1VX>4tdjlCKDe&?NVBUNE$?yd?2r-~viEr`e8g=%Ezs_Nf=WQejL zY;?kvy{0h@`&eeWBLYhsd6Vx|IsJ3_eR0^|>8Tf0HBR{2$8(CZ#O+iN(E& zp)4k(H`zqU-R2;?91M|`;?}oM(eeB2`Dd6+_`XvvX%(qY>`lm`HOsS4Y+hH)V?mYzVe{Z z%PBx_Ctkbp5+;;SXV?0KKO@Q@2`{N*%Q`^(E)Jw~&YoH`dhV`2cKuTm)(><`w)aXu zW>2RpjM--%KGtYX26_q7EAGbOQ|b#hqf$>3XlTi-dm9^4{;JmPUMYo^mZn?Zf_KuD zx1zzW33(D#6ax_b0U>LMl=J&^1e@rXb!iTdHMLz$DxV9z@`Z52jMErPdlu{j=Z`zd@}|Ra zAjel5f!9V5%|@5<>AqKSiz*9G^ZxXaO2s~GWt^J7sCC@yYS0#Im4xTj5z)1yF8)~@ z96|HA)>-x}GCA!Ta}x@NnaYC}=JnC}(jgp{x=Hn_#v1EUr!PI9qe?e)$_MwIrYt$F zseE~?4b|z$%!lWg{YzytP_wefdgItkN$5=Cv!{(6dr3dh+rRP%k!6pfXM{kqJokFTQS=LpQY8VB3@*ogL{{N|#QJ>%h3u`N0Ny-yxE#WUL5 zi~WOJ~#5`3u+6?DbUgM(g-kIRZzCO&@YIl0R(ltx9-4aE2|cl3 zTH1)t<%hFtK1K1UYc~|Ih@l=efz5C;n+5CfkXQ#7E2wh<0`c72Nm6UfNI6NZH5t7l zSx4)C$?zuqUh z>e9l#7L|B%<;%=?-hqym%^0aP=?KT&0*#bo%1j(F8kotqKA??81O2w%27h9rq=xli zW@8~Up9W*>__J2>C(0+sl@$+HPXAZQit4sYiJ^YUmyA#z#5NqB&Q0m2BXQ`Nw9={( zLrs^ezAlAxb-RQQ*T_q>;xwc(1wJY{lPs8@*~n9-@*V!n7Vof8BjGt(Vtt(+*VbS z)RfEc5FTBSzta5xomE>ks$9X3=R_<^BVKdurkB=@t+;JJrCuBtR{8CZMJyCV&D)Gc z0qiT>M4bMlpmK=2&cLvW=>8E!>kS%9r2~9_cO2p=ipLx!ky(7ZQWKp zfu`K;v1HoVV3%G`(*LE(sL62uj@{jl0@VVl)kMCMQ3w=VihZ!uOra&R) z@rqv?wTjx3bQAtwcls7OqTE&DGqDgupj0{8GhA=0R1}c4Q$n=0LZE(GM0)}oG_YC7RX8863pkMq89GH}kCbw6njikCHD`S_VJQl7#9}V*UYb@?F-i@SM z(kSK65gqXU$vod3OcQ^{!b+7O<(LRo-bY$9?4$49oHwi(>t~Lp*sIBXO?8|XM-Y~~ zN8Mqn)Gv=n2J2*XSP&7jHT>{HlMIgU)q%$1bos0=HnOe0=70M#mnK{*O1`hPLAHWz zBCM1e$1PiGHObOf=j3kgx-yPK1`juECN6?~qvqwwFVJh#h?ydJtR##<{`es?5m(*B z%pMNq7wnfwEVoVKeJhP`8W_xnBc4k}@ldJHz7ve$Pu%m&>~DTRdg`5c z#HqU~CFzS+?$j%W`o@XRp@*rU#KMM=TP_2~gG>GUbaW>t`o=@_@13@xu4ZL<(-Bzc z>a1kuvHj>PitE<>w5y5I8T_gH@{jDuBt7$elPcxk9XzHrt$Giiq`?a|N4y3?SVDf% zkd{4t!y+q`R_VhFU*8$w`&@LX^&!2_PVYe4@xzY@HC4y#2(3T+iL+TFRd=m65(gsa8oZHjWhGwI z8UK|)K<-Wn>$M$j+p(;IY&cE~B$R-zq(D z^%y^XUUf*EZlL*o!#w-TBK-szRL zPB%~SY7Lb3ak!FwoQ5}NXpfahn;pO633c2sa9ILu6V~zTHBo5az7BBQTDsZu=gGd*48OJ8`TbKc<2GQj7 zLG+*(Pv&ERDV0%SohO-4AC?A{h=?%dn0y?Ac1*3s6IgHKOZfhLqRmrM+xL$dKIXo} za&qt|*IRl4TYNnx4vinI9~^fc59K}eu(qphDkPv=D$u2&8&+2d38dKJBy}<;s6LFY zrR|6%4j|=AUZitKnpSd3YkF3P4FW4Oer4ZsU#o%ldZiT1qFPPfl2aLm(DoDAai1N* zQ0?~)g9Jl3*-hRr`u?it?}&WE9+ChauD-S8%Ei>;&`dmWth40WoPBGlB56&r%Spfa z;iD~>4Mm8HW9Ar-;i*Y0-dMnLt+<<%IzBGa@i%1ORTvdt|LbuJuFPsbBtp#VSxkNv z9<|@ip*#rLV5fWL%MF;$Q2Bw9Ek9yCFQ;-q#6!8~gs;Q(Zq@TOX~T;f8j`Tv~dmCi*qp6d+a47wia{ ztp|x@Br@Of3{(=DWl`u4OagbM{1~+hZc!qi)JV_gne`oJKwnnrJF_}D-FY*6APr{* zXxgI8fC1Z_2wQP8>q4n=rXHN^0r8oJ7|y3~bax&n7uj|OnB~L? zV6*1iUio@&(pf3OoT2R|gq|EAS;TnB_n|aau|Fw&UtW2G78`@N<)=^X9L{SFV+OfZ zrv3tb1(n-AicHs!9{1@_i9@k?v#j{>jl`2YYi>nwo0C}aq^``e$W4hfyFE*QltOHX z1w<(QmlQSv%9X>Og@$Tm_@bAxGy(FAxhj=mXwccFOb^5ALdS)|@cKN6@O=+7WMcBI zP!HDw@xu0!H#{8Zdv^F%0s@vrABwVFyswG}R7n?xl2SRbo5J_0GiM8a|IJ+CCNEV4-d_Zv>Uxi z+NNs6!W7WOmv#^(jWi_@(%1RU;;2B=KHPGD-P--k1UdAv(gI-*0| zmPYoUOzDmDyUJ3&lu;bpdu!Xe&vj97o_&4%tr@+bGMs1r>FrhGm{Nsd*R)_(-pjl3 zC#>kIG(9ZyV?yP}!=F;6T{a15~+NNo#amFosK zlg9%N7f4gxd76&7bJ1DI9wH$>a5#)F!a1>gs2tSg%Y{#Q0vZiSTz~sdVJ6#Ta*{>A1Y74 ztl0S;I=T}M9ewn$R)6_Z?A+8vfR{_dUzpQSUvH)gAj7P^)j(dZCi7;yxxlfIOqJrT z#Mb4?*8Nxqs~0RPCA@U$C-+xw*ef^=*au--pD1(iunVn~A9_6sC{hnWNL@weJ*-HG zt`%e5CeW0yn;%~(URk~}3Ib!UQK=#EwiBz@4g+4nuLl?0Eox}Oebp(mcIfI zAXaG5V5* zXGvD#1C101o)cx%7S3Giq$E(5r4#kh<%t{(gA&_p&6XFsr|xJA{?;oJqeR74^2FUY zZgxG2_h%ID3MG6us$x+pUot~I9+3IfG5Sj#lbX78GT}_JNt(@alJ#`wl5T((RcFN) ze<)(CR;S%s+noQQrs(1dk5f)!uik#XZ3x?Iro;P!F<3_5QsIHhG3C(oI^pDzIs@hS z^kcbUUI!q^b!*RObZ+)5-Q{PJgUysDH@`3`Rm_o4vA(OK#J^;_T=LA!YI*BzH4v4Z zlDV*A;2h+YkoB!P2M?wZ&X5k*?NurbSEvlTim%wtgtg`|59!9-J*KpbPf{@iU|$D= ze;Bvqq?|^)K$E0Rd&myo!{E}515GkbXGvIIUo8N*#Ni>^mqenjTt?4xtULU;k_8}Je4pGR)8vxePywNStM-h$oYBf;R`Nrb1 zKg1Feca6kHyL`p6s)fL|7T#HRK*BkfLZr>4o*Hv=juy65Up3LG;lML+L})YK>|MU4 z8lk~U@WJ{z zc*KIal_<#TSoF^y?ZeHZLYFV4#`f;KO|~|#;Wn-)VO>nHubjZ9yGiQ{AGp?9Cx(*h z2a93t8$S3h!1RJ^MhtFyG$&DYyjWQXtaZ6mo30*z$8EXZWB);V@N5OMztdTA$?v16 z%5i1b^zg82a6b^BCl<-<=Brg9+uS4w$->DVzBGW|z>tuWC4e61+8O+tI9@mP&*%WZ zVJpkJ)&Qge5q~M5PnTbPcpAKY390}S5|6okAjVk;TL%bcmTN+J%JPR&`RL1lj2V0> z$1e5#X+hKaj@3!ICWo56@aCohN!db%KYbxJEr>*+V6trdQ=zl%b8 z<>OFJuS@;c?9tPRhcVBt;_Iw@XkvbOWVtEYesCO6AHAVe$V@Ztde^cS5n{Yxl`=a6 zc%)3X1r_BIcx=jSzB9~+rG`}aFvb6p6juU3yF zPK8)nzh5YTytQDMQ4b=fl#AKbjd?z4@GMs|Thu+jS!Qj!;ULNAdq3^NxwjTZmza!u z_4f4xTIV!NRPuaQGam`d0aHZ_sFQ{y9(`Cz6(lPmLhf|)3mTB{%Lf|eMXKHkoOUtb zSsA|E2{)JC9&0tJ(2vDUpbl42Zilwe0;cwv+PP2loEaQK};wD-S&hwS`w4_=8S zob91iJO@QnBo2CaEcCG+pEa1~`}#(L^|Au@x;GjR8ha-o^zFmtCBGsYt(D5my)0?S z(GXxe)q*NfqK(#72Ac}Gx#f7@X_%WGTlPIC15_JKCxqh8X!Wq7o*~_!)A?j$a!L)v zEg%+1XqgAr)ejY_s7)^f7L#3qtbcdMz_(A2h(zN}6|2fxwKKXVDnF!`ZL8SQ$VD3D!0Nc2CgN;TkkKc zI3AD;Hj~4Z74AQ#^-{O#(AqB}g{^;w&YUnXI|D*7W=W3-(F&dQ3Qbju45P;%CiurD zetwV^_A}yn4HlzG%|j;I4WAYrP>yvIk1Jz-SjO2p#wLH)bB)arl4T#xJ~vI*t8D&m zF1`xm@YM|8IgCHN(WY`PCk@1#Z%S;x4Vbl(uW)egGi{>3jL+(DtlJw8DM_NFsUX{= z?5pYB8f4chEZ^H$(`rJ?kX0K6aX-HmBFg{cR@NjOsrTva^9Ty~Wk)Wt8MK$sp6kDN zc2cWHYK_dqk(0F6h1;|SqgQ~?wwl%6(pM>GraVo_6;_05|iJF97dL=)|kXy&! zp|bu~TX4T2vsQI<1Ep*Ugd=HF!Sa7C+C7(GH>ODrZ|~>Q6D6!B!d`vOI7M(wk|?Iv z+MO)0b$kO*jrbtdKz^CSHr(8;_H@Pec)F%TdCj>`nd26D?DcQ+ZNKvU*lnV0 zfbVYvC5k+c6){cR-@bY$i8AK(ugU9hL*MeKi@mG|bT>5ni*~Q#h^6VSEd8trc5gG-lJyOBBZNx6 z5z<^|_3I+8cH-Ixnb=?0YO$i|vitj(0cCY}T#18Q-oPWhVn_B{#3j?j9Pi`zC)3Fu zD5!8P;I<%*1)Z!xI5K|1BD!Y3%_>Yf*20Hde#6w(z%gE1bw80#_8;x7Izmm>mZOqC zH%DYemxUdKI{LOg$nxvz#25ZrpJk$HlJvydtn5C>Rb*;H`g%sw#8O|!6qH2RY+{K~ zjX2NwVpEgM7^Qnit#U^DonS^@Y71#FMuNbfOZX`vC+?SM^je_+B%x-he!&obx#ZQ= zdM(qF_NoC!6KDrn&V{?6{Zz7=`=xg_DZj8AW#t9S1pxDqH%@(*HI9Hg2nzAyqJRsg z=iWJ|NNQu`qoZGwb%6_@V|rJ+Pi8?ue;_t%NNX%036^fxGK+ z_mt;;s;B3N(!w`@9*ViHRKf;Lz+``n_BPszW>+(r>dwfS)kq{5Ao4Gnq9jE z8NDrhKVCp~yVBiUUWq(ErZ;L1XhGg%&T-L+fP3pC1PlXOG>+Q7+8jHP1;O8aBvK0) zQeG?ghlUcMr_w1KgEGJn&oaGB@yGTtYyT#~B7);>krXiU4iI5w`@(Dy|#ea|cqIOxPbl;x$ZtT~qkO#c?chdpyB~^Vr2WW-U z7QcIt40w;6TUv2w&wKqxPPH`x-utVFLcowFpi}cRA221t-o^P(V(3=~=&$p5Eqny6WO> z|3}h;Kbb-<0Bg(FSyy=f8JhIhb~AD9Y-GX=|Nm>y^AkXa->thJ|F4jtf5QxbPD||V z^?#tM|2xXCHXvVU>Y20%fwq4(r?L}xbX>1zZqhI(HwVA)I?u1yvVNmJ>Lha2xIO1AEVL}U? zU~owBm_G_$M!FB+VnH6}3*vX{B4Xj1{(YLR>+ztfSmB93llzAFt4MrtrSz@5el z%@52Q_Q)t0>wX=LomXC_rOl7u_sn$}@d$2o39yNB%$b5lWb8674-&&>XzX-OOLoTB}M-zsX1HLxzRWF;z%eH5Ppu~ZB{_NG^2m~v6@2&fR)>Us^vLW<{2|F(0r5vOC4Wu zyia9qA1<6VJ-MO3;|9f?A|}tDzuUKQJe2zryYY1&Ma!Rz_f?13E~jARSS87Go^tpd zKE&NA*f5JI1a2vq53Z!Fd1Y|VCD|xD%BZVCZbu1E0jJjm@3ckE<1mfWh8zVAbRMLm z^EB96hHYycIMe3s{~mfrefbO9b=_3mFn-Im*$wG}-D5?mpPssBzjF}kCE_Wm$&Ho1 zHvu&$kJ9aY(}&LUyqFdfxcD;%NseQ61Lfe{f+hnsh&?D?r6UO-1h5VI)p@}KvI`N@ zlz+^VAe4!?nWD)_?RTYyFMQfE7wn7bl*;JX9ZMfBoDWhzVy7Pe=)tob_rL<%7XZgz2*1sG00$3 z97DSnl&Af{={7A@I*uV%In^MxhTBTQTIy(vz8dcIq1(7dQ=1Lk>CGLKC=IPO@HuwR z9Et2ep<=)s0Wh6^+_m7Nuv0TFUp8!}zEfr!o0u%>Ij*r?F@RQu$)cCM>mZi%5;)!B z5wwh^*oZfWZ6LO^Qaue4*FTzSf~Eb&K5Ds*D?u&4E%ulX16(y|NsIy%buU*oXj^HS zZoNilyX0gBk`%(7i<);PxAeNlx)iR;nBvh5QqB^da|yal6Hg+ws3Q&?B4!OwQJ;QylRDwcF90&F2#>`0Vkb9&t;d z<@OtOkh6k1$idn{>AF|tSa3*o$U^YYBp>7j@3QF@z^fAp|K*ImRRI{m6q;v?sSJiM z&q6U$!qY+GUX?$B7iRJLq?a4VkYwHzVU5p*Z`3<4EPNx!VhNAR{g%lXe8b+PL0hHu#j6r+Z<%hwv3zDpeF5Klc8deIwm+|d%gsbEa3 z6ZYldCHAsNeTa7U%uvr0;AXPS#5AcTfssqmqBWfAqz7zZ1E6eTx4x5+4=d8T^eX+@sI0F%0DHE(pee-1DEbw;Y`(31!TCaj!UOBhFGLFF&hDx|IF0 zeP>&^Ri+!sNAK0fD4om`D*+-;_Wwj4Dto3G4Vh^raXIz8#PVRSNaN07kf=C!XB-iz z#lrg?R^{G81LqX)?u?3cS#(=7-8$k}3au^kVe6k=yYW+-=W>FBs#ogpl&NNj!e_{f zPCO^ND?|gj6ny=nfKz~;0pck@XTgiw8Xdm`;BnTBC$cP%F6lZ`hNfHRRIY56EloeA zkH79{X71~@mftN-a@(VT}k0xVgORQ_&({qxg^nL6YktNsPl zi&+big*RIMV^j58sUt;$^I<&7DvPNn)}EI0Yt_y7wG}uQ{F`=Ta?JIHyU)}rSlU)<=d~qw zKf-~YI|&~us~Tw#O?!454@}oP(sIsFgrJaOXM@Wad59uD1Z;hSrORI0@Zof0bZKIq zMG*YTOL&7{$DQ|P2%mSEfZH&{cQEV!#?mzU-(wC0kpn`Ao-f7fDvDGcuk zB|-~O7hh%^NXm-`U#OoBD-jx6(Wc$XbikVDE zSNPW2FB&!e|C64UpPI*q5y8LUsKlzd(q;|Bu@2-4xwFwTRnxaqa2Do{%2AyS+YYo$KET5}i43el|l z#GrKxOpjv`8*;MyjCgC(p}X|H0@H+V8QvZMQH}rVX=|ad^lp1gr4GS*dl=N~-mp8C zUPQjut!xCaTCfu7$iDotl$K;0zB%xcu_@e%Tz@vnE`FQwjSvMkODHQfIytd;k2v;ZA3g>1QzG zXLH2~Nz#(YxEXW!ktzitwk9tO)AHKd#X+tBb?6mmlzahLv5=TP-waw@uSNls;{+`7 zDQ5s3tGnwWNAt80WdA-}k4+V^R)c{N-3@cyAI00%NSSklEzOppEyYxX9rbve;UOSY z<%dbh@5PN(&Cx-{gq93wJ`@9Q4UIL%SQfw@4||G7s=zLtBZXbiLA!a456VW`F2HFTXU4az|YXgVT1}p zj6mjvakm{@2_x+i6MiPS5fjW;y=)u%pNX03hj0&F#BAI#$5ZkK)pl^-6!|gww06%j z2!8n`{Hn=RdSKhQPa;780l$-?V%VRq)Up&t8K#6;-RLBUao1Du(XBh)iJD64FmDdA z5B`&LZJcJzmd|+wb=;Y} v{+g6rw`siVYK%vB3T$}N*+GN4OBWBm3)v8H literal 0 HcmV?d00001 From 9ed51e791b2c82ffda88767bc64a0f856f37c572 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 15:24:08 -0800 Subject: [PATCH 12/33] (fix) default num workers --- litellm/proxy/proxy_cli.py | 9 +++- .../proxy_load_test/litellm_proxy_config.yaml | 6 +++ litellm/proxy/proxy_load_test/locustfile.py | 27 ++++++++++ .../proxy/proxy_load_test/openai_endpoint.py | 50 +++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 litellm/proxy/proxy_load_test/litellm_proxy_config.yaml create mode 100644 litellm/proxy/proxy_load_test/locustfile.py create mode 100644 litellm/proxy/proxy_load_test/openai_endpoint.py diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index f7eba02ecb..367bbbb700 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -16,6 +16,13 @@ from importlib import resources import shutil telemetry = None +default_num_workers = 1 +try: + default_num_workers = os.cpu_count() or 1 + if default_num_workers is not None and default_num_workers > 0: + default_num_workers -= 1 +except: + pass def append_query_params(url, params): @@ -57,7 +64,7 @@ def is_port_in_use(port): @click.option("--port", default=8000, help="Port to bind the server to.", envvar="PORT") @click.option( "--num_workers", - default=1, + default=default_num_workers, help="Number of gunicorn workers to spin up", envvar="NUM_WORKERS", ) diff --git a/litellm/proxy/proxy_load_test/litellm_proxy_config.yaml b/litellm/proxy/proxy_load_test/litellm_proxy_config.yaml new file mode 100644 index 0000000000..2e107d3668 --- /dev/null +++ b/litellm/proxy/proxy_load_test/litellm_proxy_config.yaml @@ -0,0 +1,6 @@ +model_list: + - model_name: gpt-3.5-turbo + litellm_params: + model: openai/my-fake-model + api_key: my-fake-key + api_base: http://0.0.0.0:8090 \ No newline at end of file diff --git a/litellm/proxy/proxy_load_test/locustfile.py b/litellm/proxy/proxy_load_test/locustfile.py new file mode 100644 index 0000000000..2cd2e2fcce --- /dev/null +++ b/litellm/proxy/proxy_load_test/locustfile.py @@ -0,0 +1,27 @@ +from locust import HttpUser, task, between + + +class MyUser(HttpUser): + wait_time = between(1, 5) + + @task + def chat_completion(self): + headers = { + "Content-Type": "application/json", + # Include any additional headers you may need for authentication, etc. + } + + # Customize the payload with "model" and "messages" keys + payload = { + "model": "gpt-3.5-turbo", + "messages": [ + {"role": "system", "content": "You are a chat bot."}, + {"role": "user", "content": "Hello, how are you?"}, + ], + # Add more data as necessary + } + + # Make a POST request to the "chat/completions" endpoint + response = self.client.post("chat/completions", json=payload, headers=headers) + + # Print or log the response if needed diff --git a/litellm/proxy/proxy_load_test/openai_endpoint.py b/litellm/proxy/proxy_load_test/openai_endpoint.py new file mode 100644 index 0000000000..b3291ce709 --- /dev/null +++ b/litellm/proxy/proxy_load_test/openai_endpoint.py @@ -0,0 +1,50 @@ +# import sys, os +# sys.path.insert( +# 0, os.path.abspath("../") +# ) # Adds the parent directory to the system path +from fastapi import FastAPI, Request, status, HTTPException, Depends +from fastapi.responses import StreamingResponse +from fastapi.security import OAuth2PasswordBearer +from fastapi.middleware.cors import CORSMiddleware + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# for completion +@app.post("/chat/completions") +@app.post("/v1/chat/completions") +async def completion(request: Request): + return { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-3.5-turbo-0125", + "system_fingerprint": "fp_44709d6fcb", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "\n\nHello there, how may I assist you today?", + }, + "logprobs": None, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21}, + } + + +if __name__ == "__main__": + import uvicorn + + # run this on 8090, 8091, 8092 and 8093 + uvicorn.run(app, host="0.0.0.0", port=8090) From 986a5267907b4263db6af8c86b462aff2808cdd8 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 16:25:54 -0800 Subject: [PATCH 13/33] (feat) disable logging per request --- litellm/main.py | 2 ++ litellm/utils.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 63649844a3..76742eb064 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -563,6 +563,7 @@ def completion( "caching_groups", "ttl", "cache", + "no-log", ] default_params = openai_params + litellm_params non_default_params = { @@ -2417,6 +2418,7 @@ def embedding( "caching_groups", "ttl", "cache", + "no-log", ] default_params = openai_params + litellm_params non_default_params = { diff --git a/litellm/utils.py b/litellm/utils.py index 38836a4bce..01fab699ad 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -2985,12 +2985,15 @@ def client(original_function): print_verbose( f"Async Wrapper: Completed Call, calling async_success_handler: {logging_obj.async_success_handler}" ) - asyncio.create_task( - logging_obj.async_success_handler(result, start_time, end_time) - ) - threading.Thread( - target=logging_obj.success_handler, args=(result, start_time, end_time) - ).start() + # check if user does not want this to be logged + if kwargs.get("no-log", False) == False: + asyncio.create_task( + logging_obj.async_success_handler(result, start_time, end_time) + ) + threading.Thread( + target=logging_obj.success_handler, + args=(result, start_time, end_time), + ).start() # RETURN RESULT if hasattr(result, "_hidden_params"): From 4ff68c85623d4c3a52cde986a69d9f29ee0c4ab5 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 16:26:25 -0800 Subject: [PATCH 14/33] (docs) no log requests --- docs/my-website/docs/proxy/enterprise.md | 51 +++++++++++++++++++----- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/docs/my-website/docs/proxy/enterprise.md b/docs/my-website/docs/proxy/enterprise.md index a4f3ea7b17..e0c5374f0e 100644 --- a/docs/my-website/docs/proxy/enterprise.md +++ b/docs/my-website/docs/proxy/enterprise.md @@ -12,14 +12,16 @@ Features here are behind a commercial license in our `/enterprise` folder. [**Se ::: Features: -- [ ] Content Moderation with LlamaGuard -- [ ] Content Moderation with Google Text Moderations -- [ ] Content Moderation with LLM Guard -- [ ] Reject calls from Blocked User list -- [ ] Reject calls (incoming / outgoing) with Banned Keywords (e.g. competitors) -- [ ] Tracking Spend for Custom Tags +- ✅ Content Moderation with LlamaGuard +- ✅ Content Moderation with Google Text Moderations +- ✅ Content Moderation with LLM Guard +- ✅ Reject calls from Blocked User list +- ✅ Reject calls (incoming / outgoing) with Banned Keywords (e.g. competitors) +- ✅ Don't log/store specific requests (eg confidential LLM requests) +- ✅ Tracking Spend for Custom Tags -## Content Moderation with LlamaGuard +## Content Moderation +### Content Moderation with LlamaGuard Currently works with Sagemaker's LlamaGuard endpoint. @@ -39,7 +41,7 @@ os.environ["AWS_SECRET_ACCESS_KEY"] = "" os.environ["AWS_REGION_NAME"] = "" ``` -### Customize LlamaGuard prompt +#### Customize LlamaGuard prompt To modify the unsafe categories llama guard evaluates against, just create your own version of [this category list](https://github.com/BerriAI/litellm/blob/main/litellm/proxy/llamaguard_prompt.txt) @@ -51,7 +53,7 @@ callbacks: ["llamaguard_moderations"] llamaguard_unsafe_content_categories: /path/to/llamaguard_prompt.txt ``` -## Content Moderation with LLM Guard +### Content Moderation with LLM Guard Set the LLM Guard API Base in your environment @@ -78,7 +80,7 @@ Expected results: LLM Guard: Received response - {"sanitized_prompt": "hello world", "is_valid": true, "scanners": { "Regex": 0.0 }} ``` -## Content Moderation with Google Text Moderation +### Content Moderation with Google Text Moderation Requires your GOOGLE_APPLICATION_CREDENTIALS to be set in your .env (same as VertexAI). @@ -89,7 +91,7 @@ litellm_settings: callbacks: ["google_text_moderation"] ``` -### Set custom confidence thresholds +#### Set custom confidence thresholds Google Moderations checks the test against several categories. [Source](https://cloud.google.com/natural-language/docs/moderating-text#safety_attribute_confidence_scores) @@ -133,6 +135,33 @@ Here are the category specific values: | "legal" | legal_threshold: 0.1 | +## Incognito Requests - Don't log anything + +When `no-log=True`, the request will **not be logged on any callbacks** and there will be **no server logs on litellm** + +```python +import openai +client = openai.OpenAI( + api_key="anything", # proxy api-key + base_url="http://0.0.0.0:8000" # litellm proxy +) + +response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages = [ + { + "role": "user", + "content": "this is a test request, write a short poem" + } + ], + extra_body={ + "no-log": True + } +) + +print(response) +``` + ## Enable Blocked User Lists If any call is made to proxy with this user id, it'll be rejected - use this if you want to let users opt-out of ai features From d6dc28f0ed2d11329d23370071fef7f903e5f599 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 16:27:53 -0800 Subject: [PATCH 15/33] (fix) proxy setting success callbacks --- litellm/proxy/proxy_config.yaml | 9 +++------ litellm/proxy/proxy_server.py | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 654a50b2f4..76c9ed04cd 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -5,12 +5,9 @@ model_list: api_base: os.environ/AZURE_API_BASE api_key: os.environ/AZURE_API_KEY api_version: "2023-07-01-preview" - - model_name: azure-gpt-3.5 - litellm_params: - model: gpt-3.5-turbo - api_key: os.environ/OPENAI_API_KEY - model_info: - access_groups: ["public"] +litellm_settings: + set_verbose: True + success_callback: ["langfuse"] router_settings: set_verbose: True debug_level: "DEBUG" \ No newline at end of file diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 19ac5c9611..810fdc0d1f 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1675,9 +1675,9 @@ class ProxyConfig: # these are litellm callbacks - "langfuse", "sentry", "wandb" else: litellm.success_callback.append(callback) - verbose_proxy_logger.debug( + print( # noqa f"{blue_color_code} Initialized Success Callbacks - {litellm.success_callback} {reset_color_code}" - ) + ) # noqa elif key == "failure_callback": litellm.failure_callback = [] From ddd231a8c23557131a099f5ea26f7b1ba5f7a4a4 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 16:46:38 -0800 Subject: [PATCH 16/33] (feat) use no-log as a litellm param --- litellm/main.py | 3 +++ litellm/utils.py | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 76742eb064..c8a8dbc1c5 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -487,6 +487,8 @@ def completion( ### ASYNC CALLS ### acompletion = kwargs.get("acompletion", False) client = kwargs.get("client", None) + ### Admin Controls ### + no_log = kwargs.get("no-log", False) ######## end of unpacking kwargs ########### openai_params = [ "functions", @@ -727,6 +729,7 @@ def completion( model_info=model_info, proxy_server_request=proxy_server_request, preset_cache_key=preset_cache_key, + no_log=no_log, ) logging.update_environment_variables( model=model, diff --git a/litellm/utils.py b/litellm/utils.py index 01fab699ad..163223ab2f 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1279,6 +1279,10 @@ class Logging: for callback in callbacks: try: + litellm_params = self.model_call_details.get("litellm_params", {}) + if litellm_params.get("no-log", False) == True: + print_verbose("no-log request, skipping logging") + continue if callback == "lite_debugger": print_verbose("reaches lite_debugger for logging!") print_verbose(f"liteDebuggerClient: {liteDebuggerClient}") @@ -1708,6 +1712,9 @@ class Logging: verbose_logger.debug(f"Async success callbacks: {callbacks}") for callback in callbacks: try: + if kwargs.get("no-log", False) == True: + print_verbose("no-log request, skipping logging") + continue if callback == "cache" and litellm.cache is not None: # set_cache once complete streaming response is built print_verbose("async success_callback: reaches cache for logging!") @@ -2986,14 +2993,13 @@ def client(original_function): f"Async Wrapper: Completed Call, calling async_success_handler: {logging_obj.async_success_handler}" ) # check if user does not want this to be logged - if kwargs.get("no-log", False) == False: - asyncio.create_task( - logging_obj.async_success_handler(result, start_time, end_time) - ) - threading.Thread( - target=logging_obj.success_handler, - args=(result, start_time, end_time), - ).start() + asyncio.create_task( + logging_obj.async_success_handler(result, start_time, end_time) + ) + threading.Thread( + target=logging_obj.success_handler, + args=(result, start_time, end_time), + ).start() # RETURN RESULT if hasattr(result, "_hidden_params"): @@ -3895,6 +3901,7 @@ def get_litellm_params( proxy_server_request=None, acompletion=None, preset_cache_key=None, + no_log=None, ): litellm_params = { "acompletion": acompletion, @@ -3911,6 +3918,7 @@ def get_litellm_params( "model_info": model_info, "proxy_server_request": proxy_server_request, "preset_cache_key": preset_cache_key, + "no-log": no_log, "stream_response": {}, # litellm_call_id: ModelResponse Dict } From 0a538fe679a066608bbf997d5d96d646f52adb90 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 16:56:20 -0800 Subject: [PATCH 17/33] (feat) use no-log to disable per request logging --- litellm/utils.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index 163223ab2f..32f0f765bd 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1281,8 +1281,13 @@ class Logging: try: litellm_params = self.model_call_details.get("litellm_params", {}) if litellm_params.get("no-log", False) == True: - print_verbose("no-log request, skipping logging") - continue + # proxy cost tracking cal backs should run + if not ( + isinstance(callback, CustomLogger) + and "_PROXY_" in callback.__class__.__name__ + ): + print_verbose("no-log request, skipping logging") + continue if callback == "lite_debugger": print_verbose("reaches lite_debugger for logging!") print_verbose(f"liteDebuggerClient: {liteDebuggerClient}") @@ -1711,6 +1716,16 @@ class Logging: callbacks = litellm._async_success_callback verbose_logger.debug(f"Async success callbacks: {callbacks}") for callback in callbacks: + # check if callback can run for this request + litellm_params = self.model_call_details.get("litellm_params", {}) + if litellm_params.get("no-log", False) == True: + # proxy cost tracking cal backs should run + if not ( + isinstance(callback, CustomLogger) + and "_PROXY_" in callback.__class__.__name__ + ): + print_verbose("no-log request, skipping logging") + continue try: if kwargs.get("no-log", False) == True: print_verbose("no-log request, skipping logging") From 37b4dde7fd3ade2eae5a5f5c6dcc11fed5e0d1a3 Mon Sep 17 00:00:00 2001 From: Guillermo Date: Sat, 9 Mar 2024 02:24:07 +0100 Subject: [PATCH 18/33] Add quickstart deploy with k8s --- docs/my-website/docs/proxy/deploy.md | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/my-website/docs/proxy/deploy.md b/docs/my-website/docs/proxy/deploy.md index 4b51f094cb..b2e48d5229 100644 --- a/docs/my-website/docs/proxy/deploy.md +++ b/docs/my-website/docs/proxy/deploy.md @@ -68,6 +68,70 @@ CMD ["--port", "4000", "--config", "config.yaml", "--detailed_debug", "--run_gun + + +Deploying a config file based litellm instance, just requires a simple deployment that loads +the config.yaml file via a config map. + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: litellm-config-file +data: + config.yaml: | + model_list: + - model_name: gpt-3.5-turbo # user-facing model alias + litellm_params: # all params accepted by litellm.completion() - https://docs.litellm.ai/docs/completion/input + model: azure/ + api_base: + api_key: + - model_name: gpt-3.5-turbo + litellm_params: + model: azure/gpt-turbo-small-ca + api_base: https://my-endpoint-canada-berri992.openai.azure.com/ + api_key: + - model_name: vllm-model + litellm_params: + model: openai/ + api_base: # e.g. http://0.0.0.0:3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: litellm-deployment + labels: + app: litellm +spec: + selector: + matchLabels: + app: litellm + template: + metadata: + labels: + app: litellm + spec: + containers: + - name: litellm + image: ghcr.io/berriai/litellm:main-latest # it is recommended to fix a version generally + ports: + - containerPort: 4000 + volumeMounts: + - name: config-volume + mountPath: /app/proxy_server_config.yaml + subPath: config.yaml + envFrom: + - secretRef: + name: litellm-secrets + volumes: + - name: config-volume + configMap: + name: litellm-config-file + +``` + + + ## Deploy with Database From bb427b465982905a54c4a5b836ae9aab93cdbe8f Mon Sep 17 00:00:00 2001 From: Guillermo Date: Sat, 9 Mar 2024 02:30:17 +0100 Subject: [PATCH 19/33] Update deploy.md --- docs/my-website/docs/proxy/deploy.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/my-website/docs/proxy/deploy.md b/docs/my-website/docs/proxy/deploy.md index b2e48d5229..0ce9c0005b 100644 --- a/docs/my-website/docs/proxy/deploy.md +++ b/docs/my-website/docs/proxy/deploy.md @@ -70,8 +70,9 @@ CMD ["--port", "4000", "--config", "config.yaml", "--detailed_debug", "--run_gun -Deploying a config file based litellm instance, just requires a simple deployment that loads -the config.yaml file via a config map. +Deploying a config file based litellm instance just requires a simple deployment that loads +the config.yaml file via a config map. Also it would be a good practice to use the env var +declaration for api keys, and attach the env vars with the api key values as an opaque secret. ```yaml apiVersion: v1 @@ -81,20 +82,19 @@ metadata: data: config.yaml: | model_list: - - model_name: gpt-3.5-turbo # user-facing model alias - litellm_params: # all params accepted by litellm.completion() - https://docs.litellm.ai/docs/completion/input - model: azure/ - api_base: - api_key: - model_name: gpt-3.5-turbo litellm_params: model: azure/gpt-turbo-small-ca api_base: https://my-endpoint-canada-berri992.openai.azure.com/ - api_key: - - model_name: vllm-model - litellm_params: - model: openai/ - api_base: # e.g. http://0.0.0.0:3000 + api_key: os.environ/CA_AZURE_OPENAI_API_KEY +--- +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: litellm-secrets +data: + CA_AZURE_OPENAI_API_KEY: bWVvd19pbV9hX2NhdA== # your api key in base64 --- apiVersion: apps/v1 kind: Deployment @@ -127,7 +127,6 @@ spec: - name: config-volume configMap: name: litellm-config-file - ``` From 5850ff470f589959cc4d23fe7b3275b649c81088 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 20:42:12 -0800 Subject: [PATCH 20/33] (feat) disable/enable logging --- litellm/_logging.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/litellm/_logging.py b/litellm/_logging.py index 438fa9743d..26693c15ec 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -31,6 +31,18 @@ def _turn_on_debug(): verbose_proxy_logger.setLevel(level=logging.DEBUG) # set proxy logs to debug +def _disable_debugging(): + verbose_logger.disabled = True + verbose_router_logger.disabled = True + verbose_proxy_logger.disabled = True + + +def _enable_debugging(): + verbose_logger.disabled = False + verbose_router_logger.disabled = False + verbose_proxy_logger.disabled = False + + def print_verbose(print_statement): try: if set_verbose: From 7004940ce4d890fca1c7cd360847103035def2bd Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Sat, 9 Mar 2024 11:55:31 +0700 Subject: [PATCH 21/33] Update Docs for Kubernetes - [+] docs(deploy.md): add tip about using versioning or SHA digests instead of latest tag --- docs/my-website/docs/proxy/deploy.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/my-website/docs/proxy/deploy.md b/docs/my-website/docs/proxy/deploy.md index 0ce9c0005b..7eb5373b4e 100644 --- a/docs/my-website/docs/proxy/deploy.md +++ b/docs/my-website/docs/proxy/deploy.md @@ -131,6 +131,9 @@ spec: +> [!TIP] +> To avoid issues with predictability, difficulties in rollback, and inconsistent environments, use versioning or SHA digests (for example, `litellm:main-v1.30.3` or `litellm@sha256:12345abcdef...`) instead of `litellm:main-latest`. + ## Deploy with Database From 789183013697dd171a6986c20c9253c70e7e0468 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 21:20:02 -0800 Subject: [PATCH 22/33] (bump) 1.30.4 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 80f5c822c1..4c00be9102 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm" -version = "1.30.3" +version = "1.30.4" description = "Library to easily interface with LLM API providers" authors = ["BerriAI"] license = "MIT" @@ -74,7 +74,7 @@ requires = ["poetry-core", "wheel"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "1.30.3" +version = "1.30.4" version_files = [ "pyproject.toml:^version" ] From 33ba57a1b51050e055a3ddecb186e33bcfb7e640 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Sat, 9 Mar 2024 12:37:18 +0700 Subject: [PATCH 23/33] Fix Docs Formatting in Website - [+] docs(deploy.md): move tip about versioning inside the tab item --- docs/my-website/docs/proxy/deploy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/my-website/docs/proxy/deploy.md b/docs/my-website/docs/proxy/deploy.md index 7eb5373b4e..496bde05f1 100644 --- a/docs/my-website/docs/proxy/deploy.md +++ b/docs/my-website/docs/proxy/deploy.md @@ -129,11 +129,11 @@ spec: name: litellm-config-file ``` - - > [!TIP] > To avoid issues with predictability, difficulties in rollback, and inconsistent environments, use versioning or SHA digests (for example, `litellm:main-v1.30.3` or `litellm@sha256:12345abcdef...`) instead of `litellm:main-latest`. + + ## Deploy with Database From eb531364486ebce736eb2c2150d5b0b110eff134 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Fri, 8 Mar 2024 22:05:39 -0800 Subject: [PATCH 24/33] (ci/cd) run again --- litellm/tests/test_streaming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/tests/test_streaming.py b/litellm/tests/test_streaming.py index c513447b02..2896b4a711 100644 --- a/litellm/tests/test_streaming.py +++ b/litellm/tests/test_streaming.py @@ -511,7 +511,7 @@ def test_completion_mistral_api_stream(): def test_completion_deep_infra_stream(): - # deep infra currently includes role in the 2nd chunk + # deep infra,currently includes role in the 2nd chunk # waiting for them to make a fix on this litellm.set_verbose = True try: From fac01f8481ecdcbd672de05c564ad898bb6763fe Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 22:23:21 -0800 Subject: [PATCH 25/33] fix(azure.py): add pre call logging for transcription calls --- litellm/llms/azure.py | 29 +++++++++++++++++++++++++++++ litellm/utils.py | 1 + tests/test_whisper.py | 3 +++ 3 files changed, 33 insertions(+) diff --git a/litellm/llms/azure.py b/litellm/llms/azure.py index e19023b035..fb0d8b621b 100644 --- a/litellm/llms/azure.py +++ b/litellm/llms/azure.py @@ -15,6 +15,7 @@ import litellm, json import httpx from .custom_httpx.azure_dall_e_2 import CustomHTTPTransport, AsyncCustomHTTPTransport from openai import AzureOpenAI, AsyncAzureOpenAI +import uuid class AzureOpenAIError(Exception): @@ -809,6 +810,19 @@ class AzureChatCompletion(BaseLLM): azure_client = AzureOpenAI(http_client=litellm.client_session, **azure_client_params) # type: ignore else: azure_client = client + + ## LOGGING + logging_obj.pre_call( + input=f"audio_file_{uuid.uuid4()}", + api_key=azure_client.api_key, + additional_args={ + "headers": {"Authorization": f"Bearer {azure_client.api_key}"}, + "api_base": azure_client._base_url._uri_reference, + "atranscription": True, + "complete_input_dict": data, + }, + ) + response = azure_client.audio.transcriptions.create( **data, timeout=timeout # type: ignore ) @@ -845,6 +859,21 @@ class AzureChatCompletion(BaseLLM): ) else: async_azure_client = client + + ## LOGGING + logging_obj.pre_call( + input=f"audio_file_{uuid.uuid4()}", + api_key=async_azure_client.api_key, + additional_args={ + "headers": { + "Authorization": f"Bearer {async_azure_client.api_key}" + }, + "api_base": async_azure_client._base_url._uri_reference, + "atranscription": True, + "complete_input_dict": data, + }, + ) + response = await async_azure_client.audio.transcriptions.create( **data, timeout=timeout ) # type: ignore diff --git a/litellm/utils.py b/litellm/utils.py index 330903f5ad..c8a03245a7 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -981,6 +981,7 @@ class Logging: curl_command = self.model_call_details # only print verbose if verbose logger is not set + if verbose_logger.level == 0: # this means verbose logger was not switched on - user is in litellm.set_verbose=True print_verbose(f"\033[92m{curl_command}\033[0m\n") diff --git a/tests/test_whisper.py b/tests/test_whisper.py index 78e885ad33..7b6a4aa015 100644 --- a/tests/test_whisper.py +++ b/tests/test_whisper.py @@ -35,6 +35,7 @@ def test_transcription(): def test_transcription_azure(): + litellm.set_verbose = True transcript = litellm.transcription( model="azure/azure-whisper", file=audio_file, @@ -46,6 +47,8 @@ def test_transcription_azure(): assert transcript.text is not None assert isinstance(transcript.text, str) + raise Exception("it worked") + # test_transcription_azure() From 5f15047a03c97192d95dd261dad0cbd66f614664 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 22:27:36 -0800 Subject: [PATCH 26/33] build(config.yml): test specific whisper endpoint --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index daa4d59ec4..1fd19905c7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -182,7 +182,7 @@ jobs: command: | pwd ls - python -m pytest -vv tests/ -x --junitxml=test-results/junit.xml --durations=5 + python -m pytest -vv tests/test_whisper.py -x --junitxml=test-results/junit.xml --durations=5 no_output_timeout: 120m # Store test results From 0432c85bf79b53e6f85d26a94610bd0a90fdbcf9 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 22:43:07 -0800 Subject: [PATCH 27/33] test(test_whisper.py): add debugging for circle ci error --- tests/test_whisper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_whisper.py b/tests/test_whisper.py index 7b6a4aa015..b409a8f338 100644 --- a/tests/test_whisper.py +++ b/tests/test_whisper.py @@ -36,6 +36,7 @@ def test_transcription(): def test_transcription_azure(): litellm.set_verbose = True + print(f"AZURE EUROPE API BASE: {os.getenv('AZURE_EUROPE_API_BASE', None)}") transcript = litellm.transcription( model="azure/azure-whisper", file=audio_file, From c0c76707a1b14deb3f3b517807965dc732ad29ca Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 22:44:22 -0800 Subject: [PATCH 28/33] test(test_whisper.py): cleanup test --- tests/test_whisper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_whisper.py b/tests/test_whisper.py index b409a8f338..62e50392cd 100644 --- a/tests/test_whisper.py +++ b/tests/test_whisper.py @@ -48,8 +48,6 @@ def test_transcription_azure(): assert transcript.text is not None assert isinstance(transcript.text, str) - raise Exception("it worked") - # test_transcription_azure() From 7ff8fa09d614d61e71265941e2940a00769f3b1c Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 22:51:17 -0800 Subject: [PATCH 29/33] test(test_whisper.py): hardcode api base --- tests/test_whisper.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_whisper.py b/tests/test_whisper.py index 62e50392cd..dfeebb161e 100644 --- a/tests/test_whisper.py +++ b/tests/test_whisper.py @@ -36,13 +36,12 @@ def test_transcription(): def test_transcription_azure(): litellm.set_verbose = True - print(f"AZURE EUROPE API BASE: {os.getenv('AZURE_EUROPE_API_BASE', None)}") transcript = litellm.transcription( model="azure/azure-whisper", file=audio_file, - api_base=os.getenv("AZURE_EUROPE_API_BASE"), + api_base="https://my-endpoint-europe-berri-992.openai.azure.com/", api_key=os.getenv("AZURE_EUROPE_API_KEY"), - api_version=os.getenv("2024-02-15-preview"), + api_version="2024-02-15-preview", ) assert transcript.text is not None @@ -57,9 +56,9 @@ async def test_transcription_async_azure(): transcript = await litellm.atranscription( model="azure/azure-whisper", file=audio_file, - api_base=os.getenv("AZURE_EUROPE_API_BASE"), + api_base="https://my-endpoint-europe-berri-992.openai.azure.com/", api_key=os.getenv("AZURE_EUROPE_API_KEY"), - api_version=os.getenv("2024-02-15-preview"), + api_version="2024-02-15-preview", ) assert transcript.text is not None From daa371ade900a93b5b6abe0cc16d6c1e37ba01e8 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 22:41:32 -0800 Subject: [PATCH 30/33] fix(utils.py): add support for anthropic params in get_supported_openai_params --- litellm/__init__.py | 2 +- litellm/utils.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index 506147166e..04c2d23c79 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -570,7 +570,7 @@ from .utils import ( _calculate_retry_after, _should_retry, get_secret, - get_mapped_model_params, + get_supported_openai_params, ) from .llms.huggingface_restapi import HuggingfaceConfig from .llms.anthropic import AnthropicConfig diff --git a/litellm/utils.py b/litellm/utils.py index 32f0f765bd..83f2a7ec1c 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -4581,7 +4581,7 @@ def get_optional_params( if stream: optional_params["stream"] = stream elif "anthropic" in model: - supported_params = get_mapped_model_params( + supported_params = get_supported_openai_params( model=model, custom_llm_provider=custom_llm_provider ) _check_valid_arg(supported_params=supported_params) @@ -5048,7 +5048,7 @@ def get_optional_params( return optional_params -def get_mapped_model_params(model: str, custom_llm_provider: str): +def get_supported_openai_params(model: str, custom_llm_provider: str): """ Returns the supported openai params for a given model + provider """ @@ -5057,6 +5057,18 @@ def get_mapped_model_params(model: str, custom_llm_provider: str): return litellm.AmazonAnthropicClaude3Config().get_supported_openai_params() else: return litellm.AmazonAnthropicConfig().get_supported_openai_params() + elif custom_llm_provider == "ollama_chat": + return litellm.OllamaChatConfig().get_supported_openai_params() + elif custom_llm_provider == "anthropic": + return [ + "stream", + "stop", + "temperature", + "top_p", + "max_tokens", + "tools", + "tool_choice", + ] def get_llm_provider( From aeb3cbc9b6f9e53a79b44b9e43c68493b672652c Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 22:49:24 -0800 Subject: [PATCH 31/33] fix(utils.py): add additional providers to get_supported_openai_params --- litellm/utils.py | 146 +++++++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 61 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index 83f2a7ec1c..3d80ddf581 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -4254,15 +4254,9 @@ def get_optional_params( ## raise exception if provider doesn't support passed in param if custom_llm_provider == "anthropic": ## check if unsupported param passed in - supported_params = [ - "stream", - "stop", - "temperature", - "top_p", - "max_tokens", - "tools", - "tool_choice", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) # handle anthropic params if stream: @@ -4286,17 +4280,9 @@ def get_optional_params( optional_params["tools"] = tools elif custom_llm_provider == "cohere": ## check if unsupported param passed in - supported_params = [ - "stream", - "temperature", - "max_tokens", - "logit_bias", - "top_p", - "frequency_penalty", - "presence_penalty", - "stop", - "n", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) # handle cohere params if stream: @@ -4897,25 +4883,9 @@ def get_optional_params( extra_body # openai client supports `extra_body` param ) elif custom_llm_provider == "openrouter": - supported_params = [ - "functions", - "function_call", - "temperature", - "top_p", - "n", - "stream", - "stop", - "max_tokens", - "presence_penalty", - "frequency_penalty", - "logit_bias", - "user", - "response_format", - "seed", - "tools", - "tool_choice", - "max_retries", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if functions is not None: @@ -4969,28 +4939,9 @@ def get_optional_params( ) else: # assume passing in params for openai/azure openai print_verbose(f"UNMAPPED PROVIDER, ASSUMING IT'S OPENAI/AZURE") - supported_params = [ - "functions", - "function_call", - "temperature", - "top_p", - "n", - "stream", - "stop", - "max_tokens", - "presence_penalty", - "frequency_penalty", - "logit_bias", - "user", - "response_format", - "seed", - "tools", - "tool_choice", - "max_retries", - "logprobs", - "top_logprobs", - "extra_headers", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider="openai" + ) _check_valid_arg(supported_params=supported_params) if functions is not None: optional_params["functions"] = functions @@ -5069,6 +5020,79 @@ def get_supported_openai_params(model: str, custom_llm_provider: str): "tools", "tool_choice", ] + elif custom_llm_provider == "cohere": + return [ + "stream", + "temperature", + "max_tokens", + "logit_bias", + "top_p", + "frequency_penalty", + "presence_penalty", + "stop", + "n", + ] + elif custom_llm_provider == "maritalk": + return [ + "stream", + "temperature", + "max_tokens", + "top_p", + "presence_penalty", + "stop", + ] + elif custom_llm_provider == "openai" or custom_llm_provider == "azure": + return [ + "functions", + "function_call", + "temperature", + "top_p", + "n", + "stream", + "stop", + "max_tokens", + "presence_penalty", + "frequency_penalty", + "logit_bias", + "user", + "response_format", + "seed", + "tools", + "tool_choice", + "max_retries", + "logprobs", + "top_logprobs", + "extra_headers", + ] + elif custom_llm_provider == "openrouter": + return [ + "functions", + "function_call", + "temperature", + "top_p", + "n", + "stream", + "stop", + "max_tokens", + "presence_penalty", + "frequency_penalty", + "logit_bias", + "user", + "response_format", + "seed", + "tools", + "tool_choice", + "max_retries", + ] + elif custom_llm_provider == "mistral": + return [ + "temperature", + "top_p", + "stream", + "max_tokens", + "tools", + "tool_choice", + ] def get_llm_provider( From fd52b502a63ab34c5ca9dfe5e5c116bba54f3305 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 23:06:26 -0800 Subject: [PATCH 32/33] fix(utils.py): *new* get_supported_openai_params() function Returns the supported openai params for a given model + provider --- litellm/utils.py | 293 +++++++++++++++++++++++++++++------------------ 1 file changed, 180 insertions(+), 113 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index 3d80ddf581..44921f72ec 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -4305,14 +4305,9 @@ def get_optional_params( optional_params["stop_sequences"] = stop elif custom_llm_provider == "maritalk": ## check if unsupported param passed in - supported_params = [ - "stream", - "temperature", - "max_tokens", - "top_p", - "presence_penalty", - "stop", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) # handle cohere params if stream: @@ -4331,14 +4326,9 @@ def get_optional_params( optional_params["stopping_tokens"] = stop elif custom_llm_provider == "replicate": ## check if unsupported param passed in - supported_params = [ - "stream", - "temperature", - "max_tokens", - "top_p", - "stop", - "seed", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if stream: @@ -4359,7 +4349,9 @@ def get_optional_params( optional_params["stop_sequences"] = stop elif custom_llm_provider == "huggingface": ## check if unsupported param passed in - supported_params = ["stream", "temperature", "max_tokens", "top_p", "stop", "n"] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) # temperature, top_p, n, stream, stop, max_tokens, n, presence_penalty default to None if temperature is not None: @@ -4398,16 +4390,9 @@ def get_optional_params( ) # since we handle translating echo, we should not send it to TGI request elif custom_llm_provider == "together_ai": ## check if unsupported param passed in - supported_params = [ - "stream", - "temperature", - "max_tokens", - "top_p", - "stop", - "frequency_penalty", - "tools", - "tool_choice", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if stream: @@ -4428,16 +4413,9 @@ def get_optional_params( optional_params["tool_choice"] = tool_choice elif custom_llm_provider == "ai21": ## check if unsupported param passed in - supported_params = [ - "stream", - "n", - "temperature", - "max_tokens", - "top_p", - "stop", - "frequency_penalty", - "presence_penalty", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if stream: @@ -4460,7 +4438,9 @@ def get_optional_params( custom_llm_provider == "palm" or custom_llm_provider == "gemini" ): # https://developers.generativeai.google/tutorials/curl_quickstart ## check if unsupported param passed in - supported_params = ["temperature", "top_p", "stream", "n", "stop", "max_tokens"] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if temperature is not None: @@ -4489,14 +4469,9 @@ def get_optional_params( ): print_verbose(f"(start) INSIDE THE VERTEX AI OPTIONAL PARAM BLOCK") ## check if unsupported param passed in - supported_params = [ - "temperature", - "top_p", - "max_tokens", - "stream", - "tools", - "tool_choice", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if temperature is not None: @@ -4526,7 +4501,9 @@ def get_optional_params( ) elif custom_llm_provider == "sagemaker": ## check if unsupported param passed in - supported_params = ["stream", "temperature", "max_tokens", "top_p", "stop", "n"] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) # temperature, top_p, n, stream, stop, max_tokens, n, presence_penalty default to None if temperature is not None: @@ -4553,8 +4530,10 @@ def get_optional_params( max_tokens = 1 optional_params["max_new_tokens"] = max_tokens elif custom_llm_provider == "bedrock": + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) if "ai21" in model: - supported_params = ["max_tokens", "temperature", "top_p", "stream"] _check_valid_arg(supported_params=supported_params) # params "maxTokens":200,"temperature":0,"topP":250,"stop_sequences":[], # https://us-west-2.console.aws.amazon.com/bedrock/home?region=us-west-2#/providers?model=j2-ultra @@ -4567,9 +4546,6 @@ def get_optional_params( if stream: optional_params["stream"] = stream elif "anthropic" in model: - supported_params = get_supported_openai_params( - model=model, custom_llm_provider=custom_llm_provider - ) _check_valid_arg(supported_params=supported_params) # anthropic params on bedrock # \"max_tokens_to_sample\":300,\"temperature\":0.5,\"top_p\":1,\"stop_sequences\":[\"\\\\n\\\\nHuman:\"]}" @@ -4586,7 +4562,6 @@ def get_optional_params( optional_params=optional_params, ) elif "amazon" in model: # amazon titan llms - supported_params = ["max_tokens", "temperature", "stop", "top_p", "stream"] _check_valid_arg(supported_params=supported_params) # see https://us-west-2.console.aws.amazon.com/bedrock/home?region=us-west-2#/providers?model=titan-large if max_tokens is not None: @@ -4603,7 +4578,6 @@ def get_optional_params( if stream: optional_params["stream"] = stream elif "meta" in model: # amazon / meta llms - supported_params = ["max_tokens", "temperature", "top_p", "stream"] _check_valid_arg(supported_params=supported_params) # see https://us-west-2.console.aws.amazon.com/bedrock/home?region=us-west-2#/providers?model=titan-large if max_tokens is not None: @@ -4615,7 +4589,6 @@ def get_optional_params( if stream: optional_params["stream"] = stream elif "cohere" in model: # cohere models on bedrock - supported_params = ["stream", "temperature", "max_tokens"] _check_valid_arg(supported_params=supported_params) # handle cohere params if stream: @@ -4625,7 +4598,6 @@ def get_optional_params( if max_tokens is not None: optional_params["max_tokens"] = max_tokens elif "mistral" in model: - supported_params = ["max_tokens", "temperature", "stop", "top_p", "stream"] _check_valid_arg(supported_params=supported_params) # mistral params on bedrock # \"max_tokens\":400,\"temperature\":0.7,\"top_p\":0.7,\"stop\":[\"\\\\n\\\\nHuman:\"]}" @@ -4669,7 +4641,9 @@ def get_optional_params( optional_params["stop_sequences"] = stop elif custom_llm_provider == "cloudflare": # https://developers.cloudflare.com/workers-ai/models/text-generation/#input - supported_params = ["max_tokens", "stream"] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if max_tokens is not None: @@ -4677,14 +4651,9 @@ def get_optional_params( if stream is not None: optional_params["stream"] = stream elif custom_llm_provider == "ollama": - supported_params = [ - "max_tokens", - "stream", - "top_p", - "temperature", - "frequency_penalty", - "stop", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if max_tokens is not None: @@ -4708,16 +4677,9 @@ def get_optional_params( non_default_params=non_default_params, optional_params=optional_params ) elif custom_llm_provider == "nlp_cloud": - supported_params = [ - "max_tokens", - "stream", - "temperature", - "top_p", - "presence_penalty", - "frequency_penalty", - "n", - "stop", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if max_tokens is not None: @@ -4737,7 +4699,9 @@ def get_optional_params( if stop is not None: optional_params["stop_sequences"] = stop elif custom_llm_provider == "petals": - supported_params = ["max_tokens", "temperature", "top_p", "stream"] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) # max_new_tokens=1,temperature=0.9, top_p=0.6 if max_tokens is not None: @@ -4749,18 +4713,9 @@ def get_optional_params( if stream: optional_params["stream"] = stream elif custom_llm_provider == "deepinfra": - supported_params = [ - "temperature", - "top_p", - "n", - "stream", - "stop", - "max_tokens", - "presence_penalty", - "frequency_penalty", - "logit_bias", - "user", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if temperature is not None: if ( @@ -4787,14 +4742,9 @@ def get_optional_params( if user: optional_params["user"] = user elif custom_llm_provider == "perplexity": - supported_params = [ - "temperature", - "top_p", - "stream", - "max_tokens", - "presence_penalty", - "frequency_penalty", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if temperature is not None: if ( @@ -4813,15 +4763,9 @@ def get_optional_params( if frequency_penalty: optional_params["frequency_penalty"] = frequency_penalty elif custom_llm_provider == "anyscale": - supported_params = [ - "temperature", - "top_p", - "stream", - "max_tokens", - "stop", - "frequency_penalty", - "presence_penalty", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) if model in [ "mistralai/Mistral-7B-Instruct-v0.1", "mistralai/Mixtral-8x7B-Instruct-v0.1", @@ -4849,14 +4793,9 @@ def get_optional_params( if max_tokens: optional_params["max_tokens"] = max_tokens elif custom_llm_provider == "mistral": - supported_params = [ - "temperature", - "top_p", - "stream", - "max_tokens", - "tools", - "tool_choice", - ] + supported_params = get_supported_openai_params( + model=model, custom_llm_provider=custom_llm_provider + ) _check_valid_arg(supported_params=supported_params) if temperature is not None: optional_params["temperature"] = temperature @@ -5002,12 +4941,27 @@ def get_optional_params( def get_supported_openai_params(model: str, custom_llm_provider: str): """ Returns the supported openai params for a given model + provider + + Example: + ``` + get_supported_openai_params(model="anthropic.claude-3", custom_llm_provider="bedrock") + ``` """ if custom_llm_provider == "bedrock": if model.startswith("anthropic.claude-3"): return litellm.AmazonAnthropicClaude3Config().get_supported_openai_params() - else: + elif model.startswith("anthropic"): return litellm.AmazonAnthropicConfig().get_supported_openai_params() + elif model.startswith("ai21"): + return ["max_tokens", "temperature", "top_p", "stream"] + elif model.startswith("amazon"): + return ["max_tokens", "temperature", "stop", "top_p", "stream"] + elif model.startswith("meta"): + return ["max_tokens", "temperature", "top_p", "stream"] + elif model.startswith("cohere"): + return ["stream", "temperature", "max_tokens"] + elif model.startswith("mistral"): + return ["max_tokens", "temperature", "stop", "top_p", "stream"] elif custom_llm_provider == "ollama_chat": return litellm.OllamaChatConfig().get_supported_openai_params() elif custom_llm_provider == "anthropic": @@ -5093,6 +5047,119 @@ def get_supported_openai_params(model: str, custom_llm_provider: str): "tools", "tool_choice", ] + elif custom_llm_provider == "replicate": + return [ + "stream", + "temperature", + "max_tokens", + "top_p", + "stop", + "seed", + ] + elif custom_llm_provider == "huggingface": + return ["stream", "temperature", "max_tokens", "top_p", "stop", "n"] + elif custom_llm_provider == "together_ai": + return [ + "stream", + "temperature", + "max_tokens", + "top_p", + "stop", + "frequency_penalty", + "tools", + "tool_choice", + ] + elif custom_llm_provider == "ai21": + return [ + "stream", + "n", + "temperature", + "max_tokens", + "top_p", + "stop", + "frequency_penalty", + "presence_penalty", + ] + elif custom_llm_provider == "palm" or custom_llm_provider == "gemini": + return ["temperature", "top_p", "stream", "n", "stop", "max_tokens"] + elif custom_llm_provider == "vertex_ai": + return [ + "temperature", + "top_p", + "max_tokens", + "stream", + "tools", + "tool_choice", + ] + elif custom_llm_provider == "sagemaker": + return ["stream", "temperature", "max_tokens", "top_p", "stop", "n"] + elif custom_llm_provider == "aleph_alpha": + return [ + "max_tokens", + "stream", + "top_p", + "temperature", + "presence_penalty", + "frequency_penalty", + "n", + "stop", + ] + elif custom_llm_provider == "cloudflare": + return ["max_tokens", "stream"] + elif custom_llm_provider == "ollama": + return [ + "max_tokens", + "stream", + "top_p", + "temperature", + "frequency_penalty", + "stop", + ] + elif custom_llm_provider == "nlp_cloud": + return [ + "max_tokens", + "stream", + "temperature", + "top_p", + "presence_penalty", + "frequency_penalty", + "n", + "stop", + ] + elif custom_llm_provider == "petals": + return ["max_tokens", "temperature", "top_p", "stream"] + elif custom_llm_provider == "deepinfra": + return [ + "temperature", + "top_p", + "n", + "stream", + "stop", + "max_tokens", + "presence_penalty", + "frequency_penalty", + "logit_bias", + "user", + ] + elif custom_llm_provider == "perplexity": + return [ + "temperature", + "top_p", + "stream", + "max_tokens", + "presence_penalty", + "frequency_penalty", + ] + elif custom_llm_provider == "anyscale": + return [ + "temperature", + "top_p", + "stream", + "max_tokens", + "stop", + "frequency_penalty", + "presence_penalty", + ] def get_llm_provider( From c15c05e46038cef1359bc804cdc598d6edcfa7bd Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 8 Mar 2024 23:07:26 -0800 Subject: [PATCH 33/33] build(config.yml): fix config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1fd19905c7..daa4d59ec4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -182,7 +182,7 @@ jobs: command: | pwd ls - python -m pytest -vv tests/test_whisper.py -x --junitxml=test-results/junit.xml --durations=5 + python -m pytest -vv tests/ -x --junitxml=test-results/junit.xml --durations=5 no_output_timeout: 120m # Store test results