From 85cc8c07a4c693308a1893c650d4ae77f00da401 Mon Sep 17 00:00:00 2001 From: Wesley George Date: Thu, 10 Apr 2025 17:03:09 -0400 Subject: [PATCH 1/8] :sparkles: Support for Featherless.ai as inference provider. --- .../inference/src/lib/getProviderHelper.ts | 6 +- .../inference/src/providers/featherless-ai.ts | 51 +++++++++++++++ packages/inference/src/types.ts | 1 + .../inference/test/InferenceClient.spec.ts | 63 +++++++++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 packages/inference/src/providers/featherless-ai.ts diff --git a/packages/inference/src/lib/getProviderHelper.ts b/packages/inference/src/lib/getProviderHelper.ts index 8c345208a..e4b969692 100644 --- a/packages/inference/src/lib/getProviderHelper.ts +++ b/packages/inference/src/lib/getProviderHelper.ts @@ -4,7 +4,7 @@ import * as Cohere from "../providers/cohere"; import * as FalAI from "../providers/fal-ai"; import * as Fireworks from "../providers/fireworks-ai"; import * as HFInference from "../providers/hf-inference"; - +import * as FeatherlessAI from "../providers/featherless-ai"; import * as Hyperbolic from "../providers/hyperbolic"; import * as Nebius from "../providers/nebius"; import * as Novita from "../providers/novita"; @@ -62,6 +62,10 @@ export const PROVIDERS: Record { + choices: Array<{ + text: string; + finish_reason: TextGenerationOutputFinishReason; + seed: number; + logprobs: unknown; + index: number; + }>; +} + +const FEATHERLESS_API_BASE_URL = "https://api.featherless.ai"; + +export class FeatherlessAIConversationalTask extends BaseConversationalTask { + constructor() { + super("featherless-ai", FEATHERLESS_API_BASE_URL); + } +} + +export class FeatherlessAITextGenerationTask extends BaseTextGenerationTask { + constructor() { + super("featherless-ai", FEATHERLESS_API_BASE_URL); + } + + override preparePayload(params: BodyParams): Record { + return { + model: params.model, + ...params.args, + ...params.args.parameters, + prompt: params.args.inputs, + }; + } + + override async getResponse(response: FeatherlessAITextCompletionOutput): Promise { + if ( + typeof response === "object" && + "choices" in response && + Array.isArray(response?.choices) && + typeof response?.model === "string" + ) { + const completion = response.choices[0]; + return { + generated_text: completion.text, + }; + } + throw new InferenceOutputError("Expected Together text generation response format"); + } +} diff --git a/packages/inference/src/types.ts b/packages/inference/src/types.ts index 390b55fc1..d95f6a5cc 100644 --- a/packages/inference/src/types.ts +++ b/packages/inference/src/types.ts @@ -41,6 +41,7 @@ export const INFERENCE_PROVIDERS = [ "cerebras", "cohere", "fal-ai", + "featherless-ai", "fireworks-ai", "hf-inference", "hyperbolic", diff --git a/packages/inference/test/InferenceClient.spec.ts b/packages/inference/test/InferenceClient.spec.ts index 04446bc6f..28cfa9e01 100644 --- a/packages/inference/test/InferenceClient.spec.ts +++ b/packages/inference/test/InferenceClient.spec.ts @@ -826,6 +826,69 @@ describe.concurrent("InferenceClient", () => { TIMEOUT ); + describe.concurrent( + "Featherless", + () => { + HARDCODED_MODEL_ID_MAPPING['featherless-ai'] = { + "meta-llama/Llama-3.1-8B": "meta-llama/Meta-Llama-3.1-8B", + "meta-llama/Llama-3.1-8B-Instruct": "meta-llama/Meta-Llama-3.1-8B-Instruct", + }; + + it("chatCompletion", async () => { + const res = await chatCompletion({ + accessToken: env.HF_FEATHERLESS_KEY ?? "dummy", + model: "meta-llama/Llama-3.1-8B-Instruct", + provider: "featherless-ai", + messages: [{ role: "user", content: "Complete this sentence with words, one plus one is equal " }], + temperature: 0.1, + }); + + expect(res).toBeDefined(); + expect(res.choices).toBeDefined(); + expect(res.choices?.length).toBeGreaterThan(0); + + if (res.choices && res.choices.length > 0) { + const completion = res.choices[0].message?.content; + expect(completion).toBeDefined(); + expect(typeof completion).toBe("string"); + expect(completion).toContain("two"); + } + }); + + it("chatCompletion stream", async () => { + const stream = chatCompletionStream({ + accessToken: env.HF_FEATHERLESS_KEY ?? "dummy", + model: "meta-llama/Llama-3.1-8B-Instruct", + provider: "featherless-ai", + messages: [{ role: "user", content: "Complete the equation 1 + 1 = , just the answer" }], + }) as AsyncGenerator; + let out = ""; + for await (const chunk of stream) { + if (chunk.choices && chunk.choices.length > 0) { + out += chunk.choices[0].delta.content; + } + } + expect(out).toContain("2"); + }); + + it("textGeneration", async () => { + const res = await textGeneration({ + accessToken: env.HF_FEATHERLESS_KEY ?? "dummy", + model: "meta-llama/Llama-3.1-8B", + provider: "featherless-ai", + inputs: "Paris is a city of ", + parameters: { + temperature: 0, + top_p: 0.01, + max_tokens: 10, + }, + }); + expect(res).toMatchObject({ generated_text: "2.2 million people, and it is the" }); + }); + }, + TIMEOUT + ); + describe.concurrent( "Replicate", () => { From 814cf4d3d96a9a3a4d84e9459388a9d9e4ef1ceb Mon Sep 17 00:00:00 2001 From: SBrandeis Date: Mon, 14 Apr 2025 17:01:33 +0200 Subject: [PATCH 2/8] lexical order in imports --- packages/inference/src/lib/getProviderHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inference/src/lib/getProviderHelper.ts b/packages/inference/src/lib/getProviderHelper.ts index e4b969692..d60f7e305 100644 --- a/packages/inference/src/lib/getProviderHelper.ts +++ b/packages/inference/src/lib/getProviderHelper.ts @@ -2,9 +2,9 @@ import * as BlackForestLabs from "../providers/black-forest-labs"; import * as Cerebras from "../providers/cerebras"; import * as Cohere from "../providers/cohere"; import * as FalAI from "../providers/fal-ai"; +import * as FeatherlessAI from "../providers/featherless-ai"; import * as Fireworks from "../providers/fireworks-ai"; import * as HFInference from "../providers/hf-inference"; -import * as FeatherlessAI from "../providers/featherless-ai"; import * as Hyperbolic from "../providers/hyperbolic"; import * as Nebius from "../providers/nebius"; import * as Novita from "../providers/novita"; From c99a3cb1cbf79989561ccfcab1f5759b0268d058 Mon Sep 17 00:00:00 2001 From: Wesley George Date: Tue, 15 Apr 2025 13:19:22 -0400 Subject: [PATCH 3/8] :pencil: adding Featherless to README as supported provider. --- packages/inference/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/inference/README.md b/packages/inference/README.md index 241ba7a52..cccb01e57 100644 --- a/packages/inference/README.md +++ b/packages/inference/README.md @@ -48,6 +48,7 @@ You can send inference requests to third-party providers with the inference clie Currently, we support the following providers: - [Fal.ai](https://fal.ai) +- [Featherless AI](https://featherless.ai) - [Fireworks AI](https://fireworks.ai) - [Hyperbolic](https://hyperbolic.xyz) - [Nebius](https://studio.nebius.ai) @@ -76,6 +77,7 @@ When authenticated with a third-party provider key, the request is made directly Only a subset of models are supported when requesting third-party providers. You can check the list of supported models per pipeline tasks here: - [Fal.ai supported models](https://huggingface.co/api/partners/fal-ai/models) +- [Featherless AI supported models](https://huggingface.co/api/partners/featherless-ai/models) - [Fireworks AI supported models](https://huggingface.co/api/partners/fireworks-ai/models) - [Hyperbolic supported models](https://huggingface.co/api/partners/hyperbolic/models) - [Nebius supported models](https://huggingface.co/api/partners/nebius/models) From edb66fa0b6ca823c4e15a1e8f55bf816cf868176 Mon Sep 17 00:00:00 2001 From: Wesley George Date: Tue, 15 Apr 2025 13:19:40 -0400 Subject: [PATCH 4/8] :wrench: fix build --- packages/inference/src/providers/consts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/inference/src/providers/consts.ts b/packages/inference/src/providers/consts.ts index 184a5d242..9e73fb121 100644 --- a/packages/inference/src/providers/consts.ts +++ b/packages/inference/src/providers/consts.ts @@ -20,6 +20,7 @@ export const HARDCODED_MODEL_ID_MAPPING: Record Date: Tue, 15 Apr 2025 13:20:15 -0400 Subject: [PATCH 5/8] :lipstick: run prettier. --- .../inference/src/providers/featherless-ai.ts | 70 +++++++++---------- .../inference/test/InferenceClient.spec.ts | 2 +- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/inference/src/providers/featherless-ai.ts b/packages/inference/src/providers/featherless-ai.ts index fe92a05c5..4673b28a9 100644 --- a/packages/inference/src/providers/featherless-ai.ts +++ b/packages/inference/src/providers/featherless-ai.ts @@ -3,49 +3,49 @@ import type { ChatCompletionOutput, TextGenerationOutputFinishReason } from "@hu import { InferenceOutputError } from "../lib/InferenceOutputError"; interface FeatherlessAITextCompletionOutput extends Omit { - choices: Array<{ - text: string; - finish_reason: TextGenerationOutputFinishReason; - seed: number; - logprobs: unknown; - index: number; - }>; + choices: Array<{ + text: string; + finish_reason: TextGenerationOutputFinishReason; + seed: number; + logprobs: unknown; + index: number; + }>; } const FEATHERLESS_API_BASE_URL = "https://api.featherless.ai"; export class FeatherlessAIConversationalTask extends BaseConversationalTask { - constructor() { - super("featherless-ai", FEATHERLESS_API_BASE_URL); - } + constructor() { + super("featherless-ai", FEATHERLESS_API_BASE_URL); + } } export class FeatherlessAITextGenerationTask extends BaseTextGenerationTask { - constructor() { - super("featherless-ai", FEATHERLESS_API_BASE_URL); - } + constructor() { + super("featherless-ai", FEATHERLESS_API_BASE_URL); + } - override preparePayload(params: BodyParams): Record { - return { - model: params.model, - ...params.args, - ...params.args.parameters, - prompt: params.args.inputs, - }; - } + override preparePayload(params: BodyParams): Record { + return { + model: params.model, + ...params.args, + ...params.args.parameters, + prompt: params.args.inputs, + }; + } - override async getResponse(response: FeatherlessAITextCompletionOutput): Promise { - if ( - typeof response === "object" && - "choices" in response && - Array.isArray(response?.choices) && - typeof response?.model === "string" - ) { - const completion = response.choices[0]; - return { - generated_text: completion.text, - }; - } - throw new InferenceOutputError("Expected Together text generation response format"); - } + override async getResponse(response: FeatherlessAITextCompletionOutput): Promise { + if ( + typeof response === "object" && + "choices" in response && + Array.isArray(response?.choices) && + typeof response?.model === "string" + ) { + const completion = response.choices[0]; + return { + generated_text: completion.text, + }; + } + throw new InferenceOutputError("Expected Together text generation response format"); + } } diff --git a/packages/inference/test/InferenceClient.spec.ts b/packages/inference/test/InferenceClient.spec.ts index 28cfa9e01..057b9fc37 100644 --- a/packages/inference/test/InferenceClient.spec.ts +++ b/packages/inference/test/InferenceClient.spec.ts @@ -829,7 +829,7 @@ describe.concurrent("InferenceClient", () => { describe.concurrent( "Featherless", () => { - HARDCODED_MODEL_ID_MAPPING['featherless-ai'] = { + HARDCODED_MODEL_ID_MAPPING["featherless-ai"] = { "meta-llama/Llama-3.1-8B": "meta-llama/Meta-Llama-3.1-8B", "meta-llama/Llama-3.1-8B-Instruct": "meta-llama/Meta-Llama-3.1-8B-Instruct", }; From 903cace3a8db2e4be52b9acbf5a3d486b8a3f04f Mon Sep 17 00:00:00 2001 From: Wesley George Date: Tue, 15 Apr 2025 13:21:26 -0400 Subject: [PATCH 6/8] :wrench: address PR feedback --- packages/inference/src/providers/featherless-ai.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/inference/src/providers/featherless-ai.ts b/packages/inference/src/providers/featherless-ai.ts index 4673b28a9..d28cfc47c 100644 --- a/packages/inference/src/providers/featherless-ai.ts +++ b/packages/inference/src/providers/featherless-ai.ts @@ -1,6 +1,7 @@ import { BaseConversationalTask, BaseTextGenerationTask } from "./providerHelper"; -import type { ChatCompletionOutput, TextGenerationOutputFinishReason } from "@huggingface/tasks"; +import type { ChatCompletionOutput, TextGenerationOutputFinishReason, TextGenerationOutput } from "@huggingface/tasks"; import { InferenceOutputError } from "../lib/InferenceOutputError"; +import type { BodyParams } from "../types"; interface FeatherlessAITextCompletionOutput extends Omit { choices: Array<{ @@ -27,9 +28,9 @@ export class FeatherlessAITextGenerationTask extends BaseTextGenerationTask { override preparePayload(params: BodyParams): Record { return { - model: params.model, ...params.args, - ...params.args.parameters, + ...(params.args.parameters as Record), + model: params.model, prompt: params.args.inputs, }; } @@ -46,6 +47,6 @@ export class FeatherlessAITextGenerationTask extends BaseTextGenerationTask { generated_text: completion.text, }; } - throw new InferenceOutputError("Expected Together text generation response format"); + throw new InferenceOutputError("Expected Featherless AI text generation response format"); } } From 0b9d5bb2d13fb17ead0d1d95dc96dc9645b1f916 Mon Sep 17 00:00:00 2001 From: Wesley George Date: Tue, 15 Apr 2025 13:19:40 -0400 Subject: [PATCH 7/8] :wrench: fix build --- packages/inference/test/InferenceClient.spec.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/inference/test/InferenceClient.spec.ts b/packages/inference/test/InferenceClient.spec.ts index 252d88ff9..28b4deaad 100644 --- a/packages/inference/test/InferenceClient.spec.ts +++ b/packages/inference/test/InferenceClient.spec.ts @@ -1049,9 +1049,19 @@ describe.concurrent("InferenceClient", () => { describe.concurrent( "Featherless", () => { - HARDCODED_MODEL_ID_MAPPING["featherless-ai"] = { - "meta-llama/Llama-3.1-8B": "meta-llama/Meta-Llama-3.1-8B", - "meta-llama/Llama-3.1-8B-Instruct": "meta-llama/Meta-Llama-3.1-8B-Instruct", + HARDCODED_MODEL_INFERENCE_MAPPING["featherless-ai"] = { + "meta-llama/Llama-3.1-8B": { + providerId: "meta-llama/Meta-Llama-3.1-8B", + hfModelId: "meta-llama/Llama-3.1-8B", + task: "text-generation", + status: "live", + }, + "meta-llama/Llama-3.1-8B-Instruct": { + providerId: "meta-llama/Meta-Llama-3.1-8B-Instruct", + hfModelId: "meta-llama/Llama-3.1-8B-Instruct", + task: "text-generation", + status: "live", + }, }; it("chatCompletion", async () => { From 5e48368c1e753f1cee23c13668e514a9b48acffb Mon Sep 17 00:00:00 2001 From: Wesley George Date: Tue, 15 Apr 2025 13:29:34 -0400 Subject: [PATCH 8/8] Update tapes.json --- packages/inference/test/tapes.json | 92 ++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/packages/inference/test/tapes.json b/packages/inference/test/tapes.json index b8096f13d..0bd502715 100644 --- a/packages/inference/test/tapes.json +++ b/packages/inference/test/tapes.json @@ -7456,5 +7456,97 @@ "server": "UploadServer" } } + }, + "114810cb7fc73df3a897d2100ff6d2aa61072ceacd2ac8419cd7d0301b5f2038": { + "url": "https://api.featherless.ai/v1/chat/completions", + "init": { + "headers": { + "Content-Type": "application/json" + }, + "method": "POST", + "body": "{\"messages\":[{\"role\":\"user\",\"content\":\"Complete this sentence with words, one plus one is equal \"}],\"temperature\":0.1,\"model\":\"meta-llama/Meta-Llama-3.1-8B-Instruct\"}" + }, + "response": { + "body": "{\"id\":\"XA2hY8\",\"object\":\"chat.completion\",\"created\":1744738127375,\"model\":\"meta-llama/Meta-Llama-3.1-8B-Instruct\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"...to two.\"},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"system_fingerprint\":\"\",\"usage\":{\"prompt_tokens\":17,\"completion_tokens\":4,\"total_tokens\":21}}", + "status": 200, + "statusText": "OK", + "headers": { + "access-control-allow-credentials": "true", + "cache-control": "no-cache", + "cf-cache-status": "DYNAMIC", + "cf-ray": "930d294a9afdaca5-YYZ", + "connection": "keep-alive", + "content-encoding": "br", + "content-type": "application/json", + "nel": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}", + "report-to": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=YgZmhbIzMEx2tnSIcGrXly33AJVRZLvogEbxvuKRW%2BywW%2FXtW2MjGmB7VQTK%2FnV6pP4tLAwxJ7L0856uiUvl8EamnWgFbIUky0%2BIsUklVJT6DVUmQm%2B3h0fJHq9qZgRT3eZWEw%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}", + "server": "cloudflare", + "server-timing": "cfL4;desc=\"?proto=TCP&rtt=29041&min_rtt=23313&rtt_var=12834&sent=4&recv=5&lost=0&retrans=0&sent_bytes=2847&recv_bytes=1024&delivery_rate=124222&cwnd=218&unsent_bytes=0&cid=e073b0090fe1f02c&ts=2740&x=0\"", + "strict-transport-security": "max-age=15724800; includeSubDomains", + "transfer-encoding": "chunked", + "vary": "Origin" + } + } + }, + "36395857d7dc0298b346b9b2755ac22dede1384a02249b01c22958e9b6dcc27d": { + "url": "https://api.featherless.ai/v1/chat/completions", + "init": { + "headers": { + "Content-Type": "application/json" + }, + "method": "POST", + "body": "{\"messages\":[{\"role\":\"user\",\"content\":\"Complete the equation 1 + 1 = , just the answer\"}],\"stream\":true,\"model\":\"meta-llama/Meta-Llama-3.1-8B-Instruct\"}" + }, + "response": { + "body": ": FEATHERLESS PROCESSING\n: FEATHERLESS PROCESSING\ndata: {\"id\":\"T1R1ho\",\"object\":\"chat.completion.chunk\",\"created\":1744738127478,\"model\":\"meta-llama/Meta-Llama-3.1-8B-Instruct\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"index\":0,\"finish_reason\":null}]}\n\ndata: {\"id\":\"T1R1ho\",\"object\":\"chat.completion.chunk\",\"created\":1744738128780,\"model\":\"meta-llama/Meta-Llama-3.1-8B-Instruct\",\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0,\"finish_reason\":null}]}\n\ndata: {\"id\":\"T1R1ho\",\"object\":\"chat.completion.chunk\",\"created\":1744738129059,\"model\":\"meta-llama/Meta-Llama-3.1-8B-Instruct\",\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0,\"finish_reason\":null}]}\n\ndata: {\"id\":\"T1R1ho\",\"object\":\"chat.completion.chunk\",\"created\":1744738129092,\"model\":\"meta-llama/Meta-Llama-3.1-8B-Instruct\",\"choices\":[{\"delta\":{\"content\":\"2\"},\"index\":0,\"finish_reason\":null}]}\n\ndata: {\"id\":\"T1R1ho\",\"object\":\"text_completion\",\"created\":1744738129671,\"model\":\"meta-llama/Meta-Llama-3.1-8B-Instruct\",\"choices\":[{\"delta\":{},\"index\":0,\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n", + "status": 200, + "statusText": "OK", + "headers": { + "access-control-allow-credentials": "true", + "cache-control": "no-cache", + "cf-cache-status": "DYNAMIC", + "cf-ray": "930d294aadf5eb61-ORD", + "connection": "keep-alive", + "content-type": "text/event-stream", + "nel": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}", + "report-to": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=7awdH7G%2FfIt%2F6A%2FCcVcyItEaJLW%2FzD5RcOVIIWM4wkWBqx6BV9BaI18U%2BTFlSiQfa5X5jEnLLmSTdgr7E7M%2B38UIxgNDezqK45dJvwzVF4pjFps1xPMG%2F2C0Vz5ylgIGs4ZxCA%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}", + "server": "cloudflare", + "server-timing": "cfL4;desc=\"?proto=TCP&rtt=35139&min_rtt=32588&rtt_var=14043&sent=4&recv=5&lost=0&retrans=0&sent_bytes=2847&recv_bytes=1010&delivery_rate=88867&cwnd=222&unsent_bytes=0&cid=70e9c03ed3d0c745&ts=3251&x=0\"", + "strict-transport-security": "max-age=15724800; includeSubDomains", + "transfer-encoding": "chunked", + "vary": "Origin" + } + } + }, + "9e6bef930cb9728402e4e3963ee107398bee0776948025369cc2036ba7ada176": { + "url": "https://api.featherless.ai/v1/completions", + "init": { + "headers": { + "Content-Type": "application/json" + }, + "method": "POST", + "body": "{\"inputs\":\"Paris is a city of \",\"parameters\":{\"temperature\":0,\"top_p\":0.01,\"max_tokens\":10},\"temperature\":0,\"top_p\":0.01,\"max_tokens\":10,\"model\":\"meta-llama/Meta-Llama-3.1-8B\",\"prompt\":\"Paris is a city of \"}" + }, + "response": { + "body": "{\"id\":\"djPvCp\",\"object\":\"text_completion\",\"created\":1744738132411,\"model\":\"meta-llama/Meta-Llama-3.1-8B\",\"choices\":[{\"index\":0,\"text\":\"2.2 million people, and it is the\",\"logprobs\":null,\"finish_reason\":\"stop\"}],\"system_fingerprint\":\"\",\"usage\":{\"prompt_tokens\":6,\"completion_tokens\":10,\"total_tokens\":16}}", + "status": 200, + "statusText": "OK", + "headers": { + "access-control-allow-credentials": "true", + "cache-control": "no-cache", + "cf-cache-status": "DYNAMIC", + "cf-ray": "930d294adddb7ca5-EWR", + "connection": "keep-alive", + "content-encoding": "br", + "content-type": "application/json", + "nel": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}", + "report-to": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=UDo4w4zRyJSxdhlgdtsAf%2BjJWPk%2FAigX2XxpJEdzRlxnbUe9RbZin%2FuOch0VxhRsjz8hCeqe1BShVb%2BzxblGyosdydMFFThBXM9uBlHs%2BwNmyOB5NYfB8jbsbpiuwClYHqT77w%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}", + "server": "cloudflare", + "server-timing": "cfL4;desc=\"?proto=TCP&rtt=43135&min_rtt=40171&rtt_var=17181&sent=4&recv=5&lost=0&retrans=0&sent_bytes=2847&recv_bytes=1060&delivery_rate=72091&cwnd=251&unsent_bytes=0&cid=fc8be36f821730f7&ts=6031&x=0\"", + "strict-transport-security": "max-age=15724800; includeSubDomains", + "transfer-encoding": "chunked", + "vary": "Origin" + } + } } } \ No newline at end of file