Thomas G. Lopes commited on
Commit
6f005bf
·
1 Parent(s): 8daeb76

org billing

Browse files
README.md CHANGED
@@ -69,5 +69,28 @@ Follow these steps to get the Inference Playground running on your local machine
69
  - **Configuration:** Adjust generation parameters like temperature, max tokens, and top-p.
70
  - **Session Management:** Save and load your conversation setups using Projects and Checkpoints.
71
  - **Code Snippets:** Generate code snippets for various languages to replicate your inference calls.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  We hope you find the Inference Playground useful for exploring and experimenting with language models!
 
69
  - **Configuration:** Adjust generation parameters like temperature, max tokens, and top-p.
70
  - **Session Management:** Save and load your conversation setups using Projects and Checkpoints.
71
  - **Code Snippets:** Generate code snippets for various languages to replicate your inference calls.
72
+ - **Organization Billing:** Specify an organization to bill usage to for Team and Enterprise accounts.
73
+
74
+ ## Organization Billing
75
+
76
+ For Team and Enterprise Hugging Face Hub organizations, you can centralize billing for all users by specifying an organization to bill usage to. This feature allows:
77
+
78
+ - **Centralized Billing:** All inference requests can be billed to your organization instead of individual user accounts
79
+ - **Usage Tracking:** Track inference usage across your organization from the organization's billing page
80
+ - **Spending Controls:** Organization administrators can set spending limits and manage provider access
81
+
82
+ ### How to Use Organization Billing
83
+
84
+ 1. **In the UI:** Navigate to the settings panel and enter your organization name in the "Billing Organization" field
85
+ 2. **In Code Snippets:** Generated code examples will automatically include the billing organization parameter
86
+ 3. **API Integration:** The playground will include the `X-HF-Bill-To` header in API requests when an organization is specified
87
+
88
+ ### Requirements
89
+
90
+ - You must be a member of a Team or Enterprise Hugging Face Hub organization
91
+ - The organization must have billing enabled
92
+ - You need appropriate permissions to bill usage to the organization
93
+
94
+ For more information about organization billing, see the [Hugging Face documentation](https://huggingface.co/docs/inference-providers/pricing#billing-for-team-and-enterprise-organizations).
95
 
96
  We hope you find the Inference Playground useful for exploring and experimenting with language models!
src/lib/components/inference-playground/billing-modal.svelte ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { billing } from "$lib/state/billing.svelte";
3
+ import Dialog from "../dialog.svelte";
4
+
5
+ interface Props {
6
+ onClose: () => void;
7
+ }
8
+
9
+ const { onClose }: Props = $props();
10
+
11
+ let inputValue = $state(billing.organization);
12
+
13
+ function handleSubmit(e: Event) {
14
+ e.preventDefault();
15
+ const form = e.target as HTMLFormElement;
16
+ const formData = new FormData(form);
17
+ const org = (formData.get("billing-org") as string).trim() ?? "";
18
+ billing.organization = org;
19
+ inputValue = org;
20
+ onClose();
21
+ }
22
+
23
+ function handleReset() {
24
+ billing.reset();
25
+ inputValue = "";
26
+ }
27
+ </script>
28
+
29
+ <Dialog title="Billing Settings" open={true} {onClose}>
30
+ <div class="space-y-4">
31
+ <form onsubmit={handleSubmit} class="space-y-4">
32
+ <div>
33
+ <div class="mb-2 flex items-center gap-2">
34
+ <label for="billing-org" class="text-sm font-medium text-gray-900 dark:text-white"> Organization Name </label>
35
+ </div>
36
+ <input
37
+ type="text"
38
+ id="billing-org"
39
+ name="billing-org"
40
+ bind:value={inputValue}
41
+ placeholder="my-org-name"
42
+ class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-3 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
43
+ />
44
+ </div>
45
+
46
+ <!-- Current Status -->
47
+ <div class="flex items-center gap-2">
48
+ <div class="h-6 w-1 rounded-full bg-neutral-600"></div>
49
+ <span class="text-sm font-medium text-neutral-800 dark:text-neutral-300">
50
+ {#if billing.organization}
51
+ Currently billing to: <strong>{billing.organization}</strong>
52
+ {:else}
53
+ Currently using personal billing
54
+ {/if}
55
+ </span>
56
+ </div>
57
+
58
+ <!-- Actions -->
59
+ <div class="flex gap-3 pt-2">
60
+ {#if billing.organization}
61
+ <button
62
+ type="button"
63
+ onclick={handleReset}
64
+ class="rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 dark:focus:ring-gray-700"
65
+ >
66
+ Reset
67
+ </button>
68
+ {/if}
69
+
70
+ <button
71
+ type="submit"
72
+ class="flex-1 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 focus:outline-none dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
73
+ >
74
+ Save Settings
75
+ </button>
76
+ </div>
77
+ </form>
78
+
79
+ <!-- Help Link -->
80
+ <div class="border-t border-gray-200 pt-4 dark:border-gray-700">
81
+ <a
82
+ href="https://huggingface.co/docs/inference-providers/pricing#billing-for-team-and-enterprise-organizations"
83
+ target="_blank"
84
+ class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
85
+ >
86
+ Learn more about organization billing →
87
+ </a>
88
+ </div>
89
+ </div>
90
+ </Dialog>
src/lib/components/inference-playground/code-snippets.svelte CHANGED
@@ -2,6 +2,7 @@
2
  import { type ConversationClass } from "$lib/state/conversations.svelte";
3
  import { structuredForbiddenProviders } from "$lib/state/models.svelte";
4
  import { token } from "$lib/state/token.svelte.js";
 
5
  import { isCustomModel } from "$lib/types.js";
6
  import {
7
  getInferenceSnippet,
@@ -54,6 +55,7 @@
54
  temperature: data.config.temperature,
55
  top_p: data.config.top_p,
56
  accessToken: tokenStr,
 
57
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
  } as any;
59
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 
2
  import { type ConversationClass } from "$lib/state/conversations.svelte";
3
  import { structuredForbiddenProviders } from "$lib/state/models.svelte";
4
  import { token } from "$lib/state/token.svelte.js";
5
+ import { billing } from "$lib/state/billing.svelte";
6
  import { isCustomModel } from "$lib/types.js";
7
  import {
8
  getInferenceSnippet,
 
55
  temperature: data.config.temperature,
56
  top_p: data.config.top_p,
57
  accessToken: tokenStr,
58
+ billTo: billing.organization || undefined,
59
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
  } as any;
61
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
src/lib/components/inference-playground/playground.svelte CHANGED
@@ -13,6 +13,7 @@
13
  import IconInfo from "~icons/carbon/information";
14
  import IconSettings from "~icons/carbon/settings";
15
  import IconShare from "~icons/carbon/share";
 
16
  import { default as IconDelete } from "~icons/carbon/trash-can";
17
  import { showShareModal } from "../share-modal.svelte";
18
  import Toaster from "../toaster.svelte";
@@ -24,11 +25,13 @@
24
  import ModelSelectorModal from "./model-selector-modal.svelte";
25
  import ModelSelector from "./model-selector.svelte";
26
  import ProjectSelect from "./project-select.svelte";
 
27
  import { TEST_IDS } from "$lib/constants.js";
28
  import MessageTextarea from "./message-textarea.svelte";
29
 
30
  let viewCode = $state(false);
31
  let viewSettings = $state(false);
 
32
 
33
  let selectCompareModelOpen = $state(false);
34
 
@@ -218,40 +221,51 @@
218
 
219
  <GenerationConfig conversation={conversations.active[0]!} />
220
 
221
- <div class="mt-auto flex items-center justify-end gap-4 whitespace-nowrap">
222
- <button
223
- onclick={() => projects.current && showShareModal(projects.current)}
224
- class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
225
- >
226
- <IconShare class="text-xs" />
227
- Share
228
- </button>
229
- <a
230
- class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
231
- href="https://huggingface.co/spaces/victor/providers-metrics"
232
- target="_blank"
233
- >
234
- <IconWaterfall class="text-xs" />
235
- Metrics
236
- </a>
237
- {#if token.value}
238
  <button
239
- onclick={token.reset}
240
  class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
241
  >
242
- <svg xmlns="http://www.w3.org/2000/svg" class="text-xs" width="1em" height="1em" viewBox="0 0 32 32">
243
- <path
244
- fill="currentColor"
245
- d="M23.216 4H26V2h-7v6h2V5.096A11.96 11.96 0 0 1 28 16c0 6.617-5.383 12-12 12v2c7.72 0 14-6.28 14-14c0-5.009-2.632-9.512-6.784-12"
246
- />
247
- <path fill="currentColor" d="M16 20a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M15 9h2v9h-2z" /><path
248
- fill="currentColor"
249
- d="M16 4V2C8.28 2 2 8.28 2 16c0 4.977 2.607 9.494 6.784 12H6v2h7v-6h-2v2.903A11.97 11.97 0 0 1 4 16C4 9.383 9.383 4 16 4"
250
- />
251
- </svg>
252
- Reset token
253
  </button>
254
- {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  </div>
256
 
257
  <div class="mt-auto hidden">
@@ -312,3 +326,7 @@
312
  onClose={() => (selectCompareModelOpen = false)}
313
  />
314
  {/if}
 
 
 
 
 
13
  import IconInfo from "~icons/carbon/information";
14
  import IconSettings from "~icons/carbon/settings";
15
  import IconShare from "~icons/carbon/share";
16
+ import IconReceipt from "~icons/carbon/receipt";
17
  import { default as IconDelete } from "~icons/carbon/trash-can";
18
  import { showShareModal } from "../share-modal.svelte";
19
  import Toaster from "../toaster.svelte";
 
25
  import ModelSelectorModal from "./model-selector-modal.svelte";
26
  import ModelSelector from "./model-selector.svelte";
27
  import ProjectSelect from "./project-select.svelte";
28
+ import BillingModal from "./billing-modal.svelte";
29
  import { TEST_IDS } from "$lib/constants.js";
30
  import MessageTextarea from "./message-textarea.svelte";
31
 
32
  let viewCode = $state(false);
33
  let viewSettings = $state(false);
34
+ let billingModalOpen = $state(false);
35
 
36
  let selectCompareModelOpen = $state(false);
37
 
 
221
 
222
  <GenerationConfig conversation={conversations.active[0]!} />
223
 
224
+ <div class="mt-auto space-y-3">
225
+ <div class="flex items-center justify-end">
226
+ <button
227
+ onclick={() => (billingModalOpen = true)}
228
+ class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
229
+ >
230
+ <IconReceipt class="text-xs" />
231
+ Billing
232
+ </button>
233
+ </div>
234
+ <div class="flex items-center justify-end gap-4 whitespace-nowrap">
 
 
 
 
 
 
235
  <button
236
+ onclick={() => projects.current && showShareModal(projects.current)}
237
  class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
238
  >
239
+ <IconShare class="text-xs" />
240
+ Share
 
 
 
 
 
 
 
 
 
241
  </button>
242
+ <a
243
+ class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
244
+ href="https://huggingface.co/spaces/victor/providers-metrics"
245
+ target="_blank"
246
+ >
247
+ <IconWaterfall class="text-xs" />
248
+ Metrics
249
+ </a>
250
+ {#if token.value}
251
+ <button
252
+ onclick={token.reset}
253
+ class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
254
+ >
255
+ <svg xmlns="http://www.w3.org/2000/svg" class="text-xs" width="1em" height="1em" viewBox="0 0 32 32">
256
+ <path
257
+ fill="currentColor"
258
+ d="M23.216 4H26V2h-7v6h2V5.096A11.96 11.96 0 0 1 28 16c0 6.617-5.383 12-12 12v2c7.72 0 14-6.28 14-14c0-5.009-2.632-9.512-6.784-12"
259
+ />
260
+ <path fill="currentColor" d="M16 20a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M15 9h2v9h-2z" /><path
261
+ fill="currentColor"
262
+ d="M16 4V2C8.28 2 2 8.28 2 16c0 4.977 2.607 9.494 6.784 12H6v2h7v-6h-2v2.903A11.97 11.97 0 0 1 4 16C4 9.383 9.383 4 16 4"
263
+ />
264
+ </svg>
265
+ Reset token
266
+ </button>
267
+ {/if}
268
+ </div>
269
  </div>
270
 
271
  <div class="mt-auto hidden">
 
326
  onClose={() => (selectCompareModelOpen = false)}
327
  />
328
  {/if}
329
+
330
+ {#if billingModalOpen}
331
+ <BillingModal onClose={() => (billingModalOpen = false)} />
332
+ {/if}
src/lib/state/billing.svelte.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PersistedState } from "runed";
2
+
3
+ const STORAGE_KEY = "hf_billing_org";
4
+
5
+ class Billing {
6
+ #organization = new PersistedState(STORAGE_KEY, "");
7
+
8
+ get organization() {
9
+ return this.#organization.current;
10
+ }
11
+
12
+ set organization(org: string) {
13
+ this.#organization.current = org;
14
+ }
15
+
16
+ reset = () => {
17
+ this.organization = "";
18
+ localStorage.removeItem(STORAGE_KEY);
19
+ };
20
+ }
21
+
22
+ export const billing = new Billing();
src/lib/utils/business.svelte.ts CHANGED
@@ -10,6 +10,7 @@ import ctxLengthData from "$lib/data/context_length.json";
10
  import { InferenceClient, snippets } from "@huggingface/inference";
11
  import { ConversationClass, type ConversationEntityMembers } from "$lib/state/conversations.svelte";
12
  import { token } from "$lib/state/token.svelte";
 
13
  import {
14
  isCustomModel,
15
  isHFModel,
@@ -169,9 +170,14 @@ async function getCompletionMetadata(
169
  } as any;
170
 
171
  // Handle HuggingFace models
 
 
 
 
 
172
  return {
173
  type: "huggingface",
174
- client: new InferenceClient(token.value),
175
  args,
176
  };
177
  }
@@ -324,6 +330,7 @@ export function getInferenceSnippet(
324
  temperature?: ConversationEntityMembers["config"]["temperature"];
325
  top_p?: ConversationEntityMembers["config"]["top_p"];
326
  structured_output?: ConversationEntityMembers["structuredOutput"];
 
327
  },
328
  ): GetInferenceSnippetReturn {
329
  const model = conversation.model;
@@ -344,7 +351,6 @@ export function getInferenceSnippet(
344
  { ...providerMapping, hfModelId: model.id } as any,
345
  { ...opts, directRequest: false },
346
  );
347
- console.log(allSnippets);
348
 
349
  return allSnippets
350
  .filter(s => s.language === language)
 
10
  import { InferenceClient, snippets } from "@huggingface/inference";
11
  import { ConversationClass, type ConversationEntityMembers } from "$lib/state/conversations.svelte";
12
  import { token } from "$lib/state/token.svelte";
13
+ import { billing } from "$lib/state/billing.svelte";
14
  import {
15
  isCustomModel,
16
  isHFModel,
 
170
  } as any;
171
 
172
  // Handle HuggingFace models
173
+ const clientOptions: ConstructorParameters<typeof InferenceClient>[1] = {};
174
+ if (billing.organization) {
175
+ clientOptions.billTo = billing.organization;
176
+ }
177
+
178
  return {
179
  type: "huggingface",
180
+ client: new InferenceClient(token.value, clientOptions),
181
  args,
182
  };
183
  }
 
330
  temperature?: ConversationEntityMembers["config"]["temperature"];
331
  top_p?: ConversationEntityMembers["config"]["top_p"];
332
  structured_output?: ConversationEntityMembers["structuredOutput"];
333
+ billTo?: string;
334
  },
335
  ): GetInferenceSnippetReturn {
336
  const model = conversation.model;
 
351
  { ...providerMapping, hfModelId: model.id } as any,
352
  { ...opts, directRequest: false },
353
  );
 
354
 
355
  return allSnippets
356
  .filter(s => s.language === language)