@tanstack/ai-groq 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,69 +1,97 @@
1
+ import { getGroqApiKeyFromEnv, withGroqDefaults } from "../utils/client.js";
2
+ import { makeGroqStructuredOutputCompatible } from "../utils/schema-converter.js";
1
3
  import OpenAI from "openai";
2
4
  import { OpenAIBaseChatCompletionsTextAdapter } from "@tanstack/openai-base";
3
- import { withGroqDefaults, getGroqApiKeyFromEnv } from "../utils/client.js";
4
- import { makeGroqStructuredOutputCompatible } from "../utils/schema-converter.js";
5
- class GroqTextAdapter extends OpenAIBaseChatCompletionsTextAdapter {
6
- kind = "text";
7
- name = "groq";
8
- constructor(config, model) {
9
- super(model, "groq", new OpenAI(withGroqDefaults(config)));
10
- }
11
- makeStructuredOutputCompatible(schema, originalRequired) {
12
- return makeGroqStructuredOutputCompatible(schema, originalRequired);
13
- }
14
- async *processStreamChunks(stream, options, aguiState) {
15
- yield* super.processStreamChunks(
16
- promoteGroqUsage(stream),
17
- options,
18
- aguiState
19
- );
20
- }
21
- /**
22
- * Surfaces Groq's reasoning deltas during streaming structured output.
23
- * Groq emits `delta.reasoning` (or legacy `delta.reasoning_content`) on
24
- * reasoning models when the caller sets `reasoning_format: 'parsed'` in
25
- * modelOptions. The base's chatStream and structuredOutputStream both
26
- * route reasoning through this hook.
27
- */
28
- extractReasoning(chunk) {
29
- const delta = chunk.choices[0]?.delta;
30
- const raw = delta?.reasoning ?? delta?.reasoning_content;
31
- if (typeof raw === "string" && raw.length > 0) {
32
- return { text: raw };
33
- }
34
- return void 0;
35
- }
36
- /**
37
- * Groq's API rejects `response_format: json_schema` together with `tools`
38
- * + `stream` (returns 400 — see Groq Structured Outputs docs:
39
- * "Streaming and tool use are not currently supported with Structured
40
- * Outputs."). Force the engine onto the legacy finalization path even
41
- * though the OpenAI Chat Completions base would otherwise opt in.
42
- */
43
- supportsCombinedToolsAndSchema() {
44
- return false;
45
- }
46
- }
5
+ //#region src/adapters/text.ts
6
+ /**
7
+ * Groq Text (Chat) Adapter
8
+ *
9
+ * Tree-shakeable adapter for Groq chat/text completion. Groq exposes an
10
+ * OpenAI-compatible Chat Completions endpoint at `/openai/v1`, so we drive
11
+ * it with the OpenAI SDK via a `baseURL` override (the same pattern as
12
+ * `ai-grok`).
13
+ *
14
+ * Quirk: when usage is present on a stream, Groq historically delivered it
15
+ * under `chunk.x_groq.usage` rather than `chunk.usage`. The override below
16
+ * promotes it to the standard location so the base's RUN_FINISHED usage
17
+ * accounting works unchanged.
18
+ */
19
+ var GroqTextAdapter = class extends OpenAIBaseChatCompletionsTextAdapter {
20
+ kind = "text";
21
+ name = "groq";
22
+ constructor(config, model) {
23
+ super(model, "groq", new OpenAI(withGroqDefaults(config)));
24
+ }
25
+ makeStructuredOutputCompatible(schema, originalRequired) {
26
+ return makeGroqStructuredOutputCompatible(schema, originalRequired);
27
+ }
28
+ async *processStreamChunks(stream, options, aguiState) {
29
+ yield* super.processStreamChunks(promoteGroqUsage(stream), options, aguiState);
30
+ }
31
+ /**
32
+ * Surfaces Groq's reasoning deltas during streaming structured output.
33
+ * Groq emits `delta.reasoning` (or legacy `delta.reasoning_content`) on
34
+ * reasoning models when the caller sets `reasoning_format: 'parsed'` in
35
+ * modelOptions. The base's chatStream and structuredOutputStream both
36
+ * route reasoning through this hook.
37
+ */
38
+ extractReasoning(chunk) {
39
+ const delta = chunk.choices[0]?.delta;
40
+ const raw = delta?.reasoning ?? delta?.reasoning_content;
41
+ if (typeof raw === "string" && raw.length > 0) return { text: raw };
42
+ }
43
+ /**
44
+ * Groq's API rejects `response_format: json_schema` together with `tools`
45
+ * + `stream` (returns 400 — see Groq Structured Outputs docs:
46
+ * "Streaming and tool use are not currently supported with Structured
47
+ * Outputs."). Force the engine onto the legacy finalization path even
48
+ * though the OpenAI Chat Completions base would otherwise opt in.
49
+ */
50
+ supportsCombinedToolsAndSchema() {
51
+ return false;
52
+ }
53
+ };
54
+ /**
55
+ * Promotes Groq's non-standard `x_groq.usage` to the standard `chunk.usage`
56
+ * slot the base reads. Pass-through for chunks that already carry usage at
57
+ * the documented location.
58
+ */
47
59
  async function* promoteGroqUsage(stream) {
48
- for await (const chunk of stream) {
49
- const groqChunk = chunk;
50
- if (!chunk.usage && groqChunk.x_groq?.usage) {
51
- yield { ...chunk, usage: groqChunk.x_groq.usage };
52
- } else {
53
- yield chunk;
54
- }
55
- }
60
+ for await (const chunk of stream) {
61
+ const groqChunk = chunk;
62
+ if (!chunk.usage && groqChunk.x_groq?.usage) yield {
63
+ ...chunk,
64
+ usage: groqChunk.x_groq.usage
65
+ };
66
+ else yield chunk;
67
+ }
56
68
  }
69
+ /**
70
+ * Creates a Groq text adapter with explicit API key.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const adapter = createGroqText('llama-3.3-70b-versatile', "gsk_...");
75
+ * ```
76
+ */
57
77
  function createGroqText(model, apiKey, config) {
58
- return new GroqTextAdapter({ apiKey, ...config }, model);
78
+ return new GroqTextAdapter({
79
+ apiKey,
80
+ ...config
81
+ }, model);
59
82
  }
83
+ /**
84
+ * Creates a Groq text adapter with API key from `GROQ_API_KEY`.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const adapter = groqText('llama-3.3-70b-versatile');
89
+ * ```
90
+ */
60
91
  function groqText(model, config) {
61
- const apiKey = getGroqApiKeyFromEnv();
62
- return createGroqText(model, apiKey, config);
92
+ return createGroqText(model, getGroqApiKeyFromEnv(), config);
63
93
  }
64
- export {
65
- GroqTextAdapter,
66
- createGroqText,
67
- groqText
68
- };
69
- //# sourceMappingURL=text.js.map
94
+ //#endregion
95
+ export { GroqTextAdapter, createGroqText, groqText };
96
+
97
+ //# sourceMappingURL=text.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'\nimport { getGroqApiKeyFromEnv, withGroqDefaults } from '../utils/client'\nimport { makeGroqStructuredOutputCompatible } from '../utils/schema-converter'\nimport type { Modality, TextOptions } from '@tanstack/ai'\nimport type {\n GROQ_CHAT_MODELS,\n GroqChatModelToolCapabilitiesByName,\n ResolveInputModalities,\n ResolveProviderOptions,\n} from '../model-meta'\nimport type { GroqMessageMetadataByModality } from '../message-types'\nimport type { GroqClientConfig } from '../utils/client'\n\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof GroqChatModelToolCapabilitiesByName\n ? NonNullable<GroqChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n/**\n * Configuration for Groq text adapter\n */\nexport interface GroqTextConfig extends GroqClientConfig {}\n\n/**\n * Re-export of the public provider options type\n */\nexport type { ExternalTextProviderOptions as GroqTextProviderOptions } from '../text/text-provider-options'\n\n/**\n * Groq Text (Chat) Adapter\n *\n * Tree-shakeable adapter for Groq chat/text completion. Groq exposes an\n * OpenAI-compatible Chat Completions endpoint at `/openai/v1`, so we drive\n * it with the OpenAI SDK via a `baseURL` override (the same pattern as\n * `ai-grok`).\n *\n * Quirk: when usage is present on a stream, Groq historically delivered it\n * under `chunk.x_groq.usage` rather than `chunk.usage`. The override below\n * promotes it to the standard location so the base's RUN_FINISHED usage\n * accounting works unchanged.\n */\nexport class GroqTextAdapter<\n TModel extends (typeof GROQ_CHAT_MODELS)[number],\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends OpenAIBaseChatCompletionsTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GroqMessageMetadataByModality,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n override readonly name = 'groq' as const\n\n constructor(config: GroqTextConfig, model: TModel) {\n super(model, 'groq', new OpenAI(withGroqDefaults(config)))\n }\n\n protected override makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n ): Record<string, any> {\n return makeGroqStructuredOutputCompatible(schema, originalRequired)\n }\n\n protected override async *processStreamChunks(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n threadId: string\n messageId: string\n hasEmittedRunStarted: boolean\n },\n ) {\n yield* super.processStreamChunks(\n promoteGroqUsage(stream),\n options,\n aguiState,\n )\n }\n\n /**\n * Surfaces Groq's reasoning deltas during streaming structured output.\n * Groq emits `delta.reasoning` (or legacy `delta.reasoning_content`) on\n * reasoning models when the caller sets `reasoning_format: 'parsed'` in\n * modelOptions. The base's chatStream and structuredOutputStream both\n * route reasoning through this hook.\n */\n protected override extractReasoning(\n chunk: OpenAI.Chat.Completions.ChatCompletionChunk,\n ): { text: string } | undefined {\n const delta = chunk.choices[0]?.delta as\n | { reasoning?: unknown; reasoning_content?: unknown }\n | undefined\n const raw = delta?.reasoning ?? delta?.reasoning_content\n if (typeof raw === 'string' && raw.length > 0) {\n return { text: raw }\n }\n return undefined\n }\n\n /**\n * Groq's API rejects `response_format: json_schema` together with `tools`\n * + `stream` (returns 400 — see Groq Structured Outputs docs:\n * \"Streaming and tool use are not currently supported with Structured\n * Outputs.\"). Force the engine onto the legacy finalization path even\n * though the OpenAI Chat Completions base would otherwise opt in.\n */\n override supportsCombinedToolsAndSchema(): boolean {\n return false\n }\n}\n\n/**\n * Promotes Groq's non-standard `x_groq.usage` to the standard `chunk.usage`\n * slot the base reads. Pass-through for chunks that already carry usage at\n * the documented location.\n */\nasync function* promoteGroqUsage(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {\n for await (const chunk of stream) {\n const groqChunk = chunk as typeof chunk & {\n x_groq?: { usage?: OpenAI.Chat.Completions.ChatCompletionChunk['usage'] }\n }\n if (!chunk.usage && groqChunk.x_groq?.usage) {\n yield { ...chunk, usage: groqChunk.x_groq.usage }\n } else {\n yield chunk\n }\n }\n}\n\n/**\n * Creates a Groq text adapter with explicit API key.\n *\n * @example\n * ```typescript\n * const adapter = createGroqText('llama-3.3-70b-versatile', \"gsk_...\");\n * ```\n */\nexport function createGroqText<\n TModel extends (typeof GROQ_CHAT_MODELS)[number],\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<GroqTextConfig, 'apiKey'>,\n): GroqTextAdapter<TModel> {\n return new GroqTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Groq text adapter with API key from `GROQ_API_KEY`.\n *\n * @example\n * ```typescript\n * const adapter = groqText('llama-3.3-70b-versatile');\n * ```\n */\nexport function groqText<TModel extends (typeof GROQ_CHAT_MODELS)[number]>(\n model: TModel,\n config?: Omit<GroqTextConfig, 'apiKey'>,\n): GroqTextAdapter<TModel> {\n const apiKey = getGroqApiKeyFromEnv()\n return createGroqText(model, apiKey, config)\n}\n"],"names":[],"mappings":";;;;AA0CO,MAAM,wBAOH,qCAMR;AAAA,EACkB,OAAO;AAAA,EACP,OAAO;AAAA,EAEzB,YAAY,QAAwB,OAAe;AACjD,UAAM,OAAO,QAAQ,IAAI,OAAO,iBAAiB,MAAM,CAAC,CAAC;AAAA,EAC3D;AAAA,EAEmB,+BACjB,QACA,kBACqB;AACrB,WAAO,mCAAmC,QAAQ,gBAAgB;AAAA,EACpE;AAAA,EAEA,OAA0B,oBACxB,QACA,SACA,WAMA;AACA,WAAO,MAAM;AAAA,MACX,iBAAiB,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASmB,iBACjB,OAC8B;AAC9B,UAAM,QAAQ,MAAM,QAAQ,CAAC,GAAG;AAGhC,UAAM,MAAM,OAAO,aAAa,OAAO;AACvC,QAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,GAAG;AAC7C,aAAO,EAAE,MAAM,IAAA;AAAA,IACjB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASS,iCAA0C;AACjD,WAAO;AAAA,EACT;AACF;AAOA,gBAAgB,iBACd,QAC4D;AAC5D,mBAAiB,SAAS,QAAQ;AAChC,UAAM,YAAY;AAGlB,QAAI,CAAC,MAAM,SAAS,UAAU,QAAQ,OAAO;AAC3C,YAAM,EAAE,GAAG,OAAO,OAAO,UAAU,OAAO,MAAA;AAAA,IAC5C,OAAO;AACL,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAUO,SAAS,eAGd,OACA,QACA,QACyB;AACzB,SAAO,IAAI,gBAAgB,EAAE,QAAQ,GAAG,OAAA,GAAU,KAAK;AACzD;AAUO,SAAS,SACd,OACA,QACyB;AACzB,QAAM,SAAS,qBAAA;AACf,SAAO,eAAe,OAAO,QAAQ,MAAM;AAC7C;"}
1
+ {"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'\nimport { getGroqApiKeyFromEnv, withGroqDefaults } from '../utils/client'\nimport { makeGroqStructuredOutputCompatible } from '../utils/schema-converter'\nimport type { Modality, TextOptions } from '@tanstack/ai'\nimport type {\n GROQ_CHAT_MODELS,\n GroqChatModelToolCapabilitiesByName,\n ResolveInputModalities,\n ResolveProviderOptions,\n} from '../model-meta'\nimport type { GroqMessageMetadataByModality } from '../message-types'\nimport type { GroqClientConfig } from '../utils/client'\n\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof GroqChatModelToolCapabilitiesByName\n ? NonNullable<GroqChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n/**\n * Configuration for Groq text adapter\n */\nexport interface GroqTextConfig extends GroqClientConfig {}\n\n/**\n * Re-export of the public provider options type\n */\nexport type { ExternalTextProviderOptions as GroqTextProviderOptions } from '../text/text-provider-options'\n\n/**\n * Groq Text (Chat) Adapter\n *\n * Tree-shakeable adapter for Groq chat/text completion. Groq exposes an\n * OpenAI-compatible Chat Completions endpoint at `/openai/v1`, so we drive\n * it with the OpenAI SDK via a `baseURL` override (the same pattern as\n * `ai-grok`).\n *\n * Quirk: when usage is present on a stream, Groq historically delivered it\n * under `chunk.x_groq.usage` rather than `chunk.usage`. The override below\n * promotes it to the standard location so the base's RUN_FINISHED usage\n * accounting works unchanged.\n */\nexport class GroqTextAdapter<\n TModel extends (typeof GROQ_CHAT_MODELS)[number],\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends OpenAIBaseChatCompletionsTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GroqMessageMetadataByModality,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n override readonly name = 'groq' as const\n\n constructor(config: GroqTextConfig, model: TModel) {\n super(model, 'groq', new OpenAI(withGroqDefaults(config)))\n }\n\n protected override makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n ): Record<string, any> {\n return makeGroqStructuredOutputCompatible(schema, originalRequired)\n }\n\n protected override async *processStreamChunks(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n threadId: string\n messageId: string\n hasEmittedRunStarted: boolean\n },\n ) {\n yield* super.processStreamChunks(\n promoteGroqUsage(stream),\n options,\n aguiState,\n )\n }\n\n /**\n * Surfaces Groq's reasoning deltas during streaming structured output.\n * Groq emits `delta.reasoning` (or legacy `delta.reasoning_content`) on\n * reasoning models when the caller sets `reasoning_format: 'parsed'` in\n * modelOptions. The base's chatStream and structuredOutputStream both\n * route reasoning through this hook.\n */\n protected override extractReasoning(\n chunk: OpenAI.Chat.Completions.ChatCompletionChunk,\n ): { text: string } | undefined {\n const delta = chunk.choices[0]?.delta as\n | { reasoning?: unknown; reasoning_content?: unknown }\n | undefined\n const raw = delta?.reasoning ?? delta?.reasoning_content\n if (typeof raw === 'string' && raw.length > 0) {\n return { text: raw }\n }\n return undefined\n }\n\n /**\n * Groq's API rejects `response_format: json_schema` together with `tools`\n * + `stream` (returns 400 — see Groq Structured Outputs docs:\n * \"Streaming and tool use are not currently supported with Structured\n * Outputs.\"). Force the engine onto the legacy finalization path even\n * though the OpenAI Chat Completions base would otherwise opt in.\n */\n override supportsCombinedToolsAndSchema(): boolean {\n return false\n }\n}\n\n/**\n * Promotes Groq's non-standard `x_groq.usage` to the standard `chunk.usage`\n * slot the base reads. Pass-through for chunks that already carry usage at\n * the documented location.\n */\nasync function* promoteGroqUsage(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {\n for await (const chunk of stream) {\n const groqChunk = chunk as typeof chunk & {\n x_groq?: { usage?: OpenAI.Chat.Completions.ChatCompletionChunk['usage'] }\n }\n if (!chunk.usage && groqChunk.x_groq?.usage) {\n yield { ...chunk, usage: groqChunk.x_groq.usage }\n } else {\n yield chunk\n }\n }\n}\n\n/**\n * Creates a Groq text adapter with explicit API key.\n *\n * @example\n * ```typescript\n * const adapter = createGroqText('llama-3.3-70b-versatile', \"gsk_...\");\n * ```\n */\nexport function createGroqText<\n TModel extends (typeof GROQ_CHAT_MODELS)[number],\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<GroqTextConfig, 'apiKey'>,\n): GroqTextAdapter<TModel> {\n return new GroqTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Groq text adapter with API key from `GROQ_API_KEY`.\n *\n * @example\n * ```typescript\n * const adapter = groqText('llama-3.3-70b-versatile');\n * ```\n */\nexport function groqText<TModel extends (typeof GROQ_CHAT_MODELS)[number]>(\n model: TModel,\n config?: Omit<GroqTextConfig, 'apiKey'>,\n): GroqTextAdapter<TModel> {\n const apiKey = getGroqApiKeyFromEnv()\n return createGroqText(model, apiKey, config)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0CA,IAAa,kBAAb,cAOU,qCAMR;CACA,OAAyB;CACzB,OAAyB;CAEzB,YAAY,QAAwB,OAAe;EACjD,MAAM,OAAO,QAAQ,IAAI,OAAO,iBAAiB,MAAM,CAAC,CAAC;CAC3D;CAEA,+BACE,QACA,kBACqB;EACrB,OAAO,mCAAmC,QAAQ,gBAAgB;CACpE;CAEA,OAA0B,oBACxB,QACA,SACA,WAMA;EACA,OAAO,MAAM,oBACX,iBAAiB,MAAM,GACvB,SACA,SACF;CACF;;;;;;;;CASA,iBACE,OAC8B;EAC9B,MAAM,QAAQ,MAAM,QAAQ,EAAE,EAAE;EAGhC,MAAM,MAAM,OAAO,aAAa,OAAO;EACvC,IAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,GAC1C,OAAO,EAAE,MAAM,IAAI;CAGvB;;;;;;;;CASA,iCAAmD;EACjD,OAAO;CACT;AACF;;;;;;AAOA,gBAAgB,iBACd,QAC4D;CAC5D,WAAW,MAAM,SAAS,QAAQ;EAChC,MAAM,YAAY;EAGlB,IAAI,CAAC,MAAM,SAAS,UAAU,QAAQ,OACpC,MAAM;GAAE,GAAG;GAAO,OAAO,UAAU,OAAO;EAAM;OAEhD,MAAM;CAEV;AACF;;;;;;;;;AAUA,SAAgB,eAGd,OACA,QACA,QACyB;CACzB,OAAO,IAAI,gBAAgB;EAAE;EAAQ,GAAG;CAAO,GAAG,KAAK;AACzD;;;;;;;;;AAUA,SAAgB,SACd,OACA,QACyB;CAEzB,OAAO,eAAe,OADP,qBACc,GAAQ,MAAM;AAC7C"}
@@ -1,179 +1,217 @@
1
+ import { getGroqApiKeyFromEnv, withGroqDefaults } from "../utils/client.js";
2
+ import { base64ToArrayBuffer, generateId } from "@tanstack/ai-utils";
1
3
  import { BaseTranscriptionAdapter } from "@tanstack/ai/adapters";
2
- import { generateId, base64ToArrayBuffer } from "@tanstack/ai-utils";
3
- import { withGroqDefaults, getGroqApiKeyFromEnv } from "../utils/client.js";
4
+ //#region src/adapters/transcription.ts
5
+ /**
6
+ * Flattens the `openai` SDK's `HeadersLike` config value into a plain record so
7
+ * it can be merged into the raw `fetch` request this adapter issues. Handles
8
+ * the shapes callers actually pass (`Headers`, an entries array, or a plain
9
+ * object); null/undefined values are dropped.
10
+ *
11
+ * ponytail: doesn't unwrap the SDK's internal `NullableHeaders` class; forward
12
+ * that shape here if the SDK ever hands it to adapter config.
13
+ */
4
14
  function normalizeHeaders(headers) {
5
- const out = {};
6
- if (!headers) return out;
7
- const assign = (key, value) => {
8
- if (value != null) out[key] = String(value);
9
- };
10
- if (headers instanceof Headers) {
11
- headers.forEach((value, key) => assign(key, value));
12
- } else if (Array.isArray(headers)) {
13
- for (const [key, value] of headers) assign(key, value);
14
- } else {
15
- for (const [key, value] of Object.entries(headers)) assign(key, value);
16
- }
17
- return out;
18
- }
19
- class GroqTranscriptionAdapter extends BaseTranscriptionAdapter {
20
- name = "groq";
21
- apiKey;
22
- baseURL;
23
- defaultHeaders;
24
- constructor(config, model) {
25
- super(model, {});
26
- const resolved = withGroqDefaults(config);
27
- this.apiKey = resolved.apiKey;
28
- this.baseURL = resolved.baseURL ?? "https://api.groq.com/openai/v1";
29
- this.defaultHeaders = normalizeHeaders(resolved.defaultHeaders);
30
- }
31
- async transcribe(options) {
32
- const { model, audio, language, prompt, responseFormat, modelOptions } = options;
33
- if (responseFormat === "srt" || responseFormat === "vtt") {
34
- throw new Error(
35
- `Groq transcription does not support responseFormat='${responseFormat}'. Supported values: 'json', 'text', 'verbose_json'.`
36
- );
37
- }
38
- const effectiveFormat = responseFormat ?? "verbose_json";
39
- const useVerbose = effectiveFormat === "verbose_json";
40
- const form = new FormData();
41
- form.append("model", model);
42
- form.append("response_format", effectiveFormat);
43
- if (language !== void 0) form.append("language", language);
44
- if (prompt !== void 0) form.append("prompt", prompt);
45
- if (modelOptions?.temperature !== void 0) {
46
- form.append("temperature", String(modelOptions.temperature));
47
- }
48
- if (modelOptions?.timestamp_granularities !== void 0) {
49
- for (const g of modelOptions.timestamp_granularities) {
50
- form.append("timestamp_granularities[]", g);
51
- }
52
- }
53
- if (typeof audio === "string" && /^https?:\/\//.test(audio)) {
54
- form.append("url", audio);
55
- } else {
56
- form.append("file", this.prepareAudioFile(audio));
57
- }
58
- try {
59
- options.logger.request(
60
- `activity=transcription provider=${this.name} model=${model} verbose=${useVerbose}`,
61
- { provider: this.name, model }
62
- );
63
- const response = await fetch(`${this.baseURL}/audio/transcriptions`, {
64
- method: "POST",
65
- headers: {
66
- ...this.defaultHeaders,
67
- Authorization: `Bearer ${this.apiKey}`
68
- },
69
- body: form
70
- });
71
- if (!response.ok) {
72
- const body = await response.json().catch(() => null);
73
- const message = body?.error?.message ?? `Groq API error ${response.status}`;
74
- throw new Error(message);
75
- }
76
- if (useVerbose) {
77
- const data = await response.json();
78
- const requestId = data.x_groq?.id ?? generateId(this.name);
79
- const segments = data.segments?.map(
80
- (seg) => ({
81
- id: seg.id,
82
- start: seg.start,
83
- end: seg.end,
84
- text: seg.text,
85
- confidence: Math.exp(seg.avg_logprob)
86
- })
87
- );
88
- const words = data.words?.map((w) => ({
89
- word: w.word,
90
- start: w.start,
91
- end: w.end
92
- }));
93
- return {
94
- id: requestId,
95
- model,
96
- text: data.text,
97
- ...data.language !== void 0 && { language: data.language },
98
- ...data.duration !== void 0 && { duration: data.duration },
99
- ...segments !== void 0 && { segments },
100
- ...words !== void 0 && { words }
101
- };
102
- } else if (effectiveFormat === "text") {
103
- const text = await response.text();
104
- return {
105
- id: generateId(this.name),
106
- model,
107
- text,
108
- ...language !== void 0 && { language }
109
- };
110
- } else {
111
- const data = await response.json();
112
- return {
113
- id: data.x_groq?.id ?? generateId(this.name),
114
- model,
115
- text: data.text,
116
- ...language !== void 0 && { language }
117
- };
118
- }
119
- } catch (error) {
120
- options.logger.errors(`${this.name}.transcribe fatal`, {
121
- error,
122
- source: `${this.name}.transcribe`
123
- });
124
- throw error;
125
- }
126
- }
127
- prepareAudioFile(audio) {
128
- if (typeof File !== "undefined" && audio instanceof File) {
129
- return audio;
130
- }
131
- if (typeof Blob !== "undefined" && audio instanceof Blob) {
132
- this.ensureFileSupport();
133
- return new File([audio], "audio.mp3", {
134
- type: audio.type || "audio/mpeg"
135
- });
136
- }
137
- if (typeof ArrayBuffer !== "undefined" && audio instanceof ArrayBuffer) {
138
- this.ensureFileSupport();
139
- return new File([audio], "audio.mp3", { type: "audio/mpeg" });
140
- }
141
- if (typeof audio === "string") {
142
- this.ensureFileSupport();
143
- if (audio.startsWith("data:")) {
144
- const parts = audio.split(",");
145
- const header = parts[0];
146
- const base64Data = parts[1] || "";
147
- const mimeMatch = header?.match(/data:([^;]+)/);
148
- const mimeType = mimeMatch?.[1] || "audio/mpeg";
149
- const bytes2 = base64ToArrayBuffer(base64Data);
150
- const extension = mimeType.split("/")[1] || "mp3";
151
- return new File([bytes2], `audio.${extension}`, { type: mimeType });
152
- }
153
- const bytes = base64ToArrayBuffer(audio);
154
- return new File([bytes], "audio.mp3", { type: "audio/mpeg" });
155
- }
156
- throw new Error("Invalid audio input type");
157
- }
158
- // Throws on Node < 20 where the global `File` constructor is unavailable.
159
- ensureFileSupport() {
160
- if (typeof File === "undefined") {
161
- throw new Error(
162
- "`File` is not available in this environment. Use Node.js 20 or newer, or pass a File object directly."
163
- );
164
- }
165
- }
15
+ const out = {};
16
+ if (!headers) return out;
17
+ const assign = (key, value) => {
18
+ if (value != null) out[key] = String(value);
19
+ };
20
+ if (headers instanceof Headers) headers.forEach((value, key) => assign(key, value));
21
+ else if (Array.isArray(headers)) for (const [key, value] of headers) assign(key, value);
22
+ else for (const [key, value] of Object.entries(headers)) assign(key, value);
23
+ return out;
166
24
  }
25
+ /**
26
+ * Groq Transcription (Speech-to-Text) Adapter
27
+ *
28
+ * Tree-shakeable adapter for Groq audio transcription. Supports
29
+ * whisper-large-v3 and whisper-large-v3-turbo.
30
+ *
31
+ * Features:
32
+ * - Audio file uploads (File, Blob, ArrayBuffer, base64/data URL)
33
+ * - Remote audio URLs passed directly via Groq's `url` field — no upload needed
34
+ * - Verbose JSON response with segment and word timestamps
35
+ * - Language detection or specification (ISO-639-1)
36
+ * - Confidence scores derived from segment avg_logprob
37
+ */
38
+ var GroqTranscriptionAdapter = class extends BaseTranscriptionAdapter {
39
+ name = "groq";
40
+ apiKey;
41
+ baseURL;
42
+ defaultHeaders;
43
+ constructor(config, model) {
44
+ super(model, {});
45
+ const resolved = withGroqDefaults(config);
46
+ this.apiKey = resolved.apiKey;
47
+ this.baseURL = resolved.baseURL ?? "https://api.groq.com/openai/v1";
48
+ this.defaultHeaders = normalizeHeaders(resolved.defaultHeaders);
49
+ }
50
+ async transcribe(options) {
51
+ const { model, audio, language, prompt, responseFormat, modelOptions } = options;
52
+ if (responseFormat === "srt" || responseFormat === "vtt") throw new Error(`Groq transcription does not support responseFormat='${responseFormat}'. Supported values: 'json', 'text', 'verbose_json'.`);
53
+ const effectiveFormat = responseFormat ?? "verbose_json";
54
+ const useVerbose = effectiveFormat === "verbose_json";
55
+ const form = new FormData();
56
+ form.append("model", model);
57
+ form.append("response_format", effectiveFormat);
58
+ if (language !== void 0) form.append("language", language);
59
+ if (prompt !== void 0) form.append("prompt", prompt);
60
+ if (modelOptions?.temperature !== void 0) form.append("temperature", String(modelOptions.temperature));
61
+ if (modelOptions?.timestamp_granularities !== void 0) for (const g of modelOptions.timestamp_granularities) form.append("timestamp_granularities[]", g);
62
+ if (typeof audio === "string" && /^https?:\/\//.test(audio)) form.append("url", audio);
63
+ else form.append("file", this.prepareAudioFile(audio));
64
+ try {
65
+ options.logger.request(`activity=transcription provider=${this.name} model=${model} verbose=${useVerbose}`, {
66
+ provider: this.name,
67
+ model
68
+ });
69
+ const response = await fetch(`${this.baseURL}/audio/transcriptions`, {
70
+ method: "POST",
71
+ headers: {
72
+ ...this.defaultHeaders,
73
+ Authorization: `Bearer ${this.apiKey}`
74
+ },
75
+ body: form
76
+ });
77
+ if (!response.ok) {
78
+ const message = ((await response.json().catch(() => null))?.error)?.message ?? `Groq API error ${response.status}`;
79
+ throw new Error(message);
80
+ }
81
+ if (useVerbose) {
82
+ const data = await response.json();
83
+ const requestId = data.x_groq?.id ?? generateId(this.name);
84
+ const segments = data.segments?.map((seg) => ({
85
+ id: seg.id,
86
+ start: seg.start,
87
+ end: seg.end,
88
+ text: seg.text,
89
+ confidence: Math.exp(seg.avg_logprob)
90
+ }));
91
+ const words = data.words?.map((w) => ({
92
+ word: w.word,
93
+ start: w.start,
94
+ end: w.end
95
+ }));
96
+ return {
97
+ id: requestId,
98
+ model,
99
+ text: data.text,
100
+ ...data.language !== void 0 && { language: data.language },
101
+ ...data.duration !== void 0 && { duration: data.duration },
102
+ ...segments !== void 0 && { segments },
103
+ ...words !== void 0 && { words }
104
+ };
105
+ } else if (effectiveFormat === "text") {
106
+ const text = await response.text();
107
+ return {
108
+ id: generateId(this.name),
109
+ model,
110
+ text,
111
+ ...language !== void 0 && { language }
112
+ };
113
+ } else {
114
+ const data = await response.json();
115
+ return {
116
+ id: data.x_groq?.id ?? generateId(this.name),
117
+ model,
118
+ text: data.text,
119
+ ...language !== void 0 && { language }
120
+ };
121
+ }
122
+ } catch (error) {
123
+ options.logger.errors(`${this.name}.transcribe fatal`, {
124
+ error,
125
+ source: `${this.name}.transcribe`
126
+ });
127
+ throw error;
128
+ }
129
+ }
130
+ prepareAudioFile(audio) {
131
+ if (typeof File !== "undefined" && audio instanceof File) return audio;
132
+ if (typeof Blob !== "undefined" && audio instanceof Blob) {
133
+ this.ensureFileSupport();
134
+ return new File([audio], "audio.mp3", { type: audio.type || "audio/mpeg" });
135
+ }
136
+ if (typeof ArrayBuffer !== "undefined" && audio instanceof ArrayBuffer) {
137
+ this.ensureFileSupport();
138
+ return new File([audio], "audio.mp3", { type: "audio/mpeg" });
139
+ }
140
+ if (typeof audio === "string") {
141
+ this.ensureFileSupport();
142
+ if (audio.startsWith("data:")) {
143
+ const parts = audio.split(",");
144
+ const header = parts[0];
145
+ const base64Data = parts[1] || "";
146
+ const mimeType = (header?.match(/data:([^;]+)/))?.[1] || "audio/mpeg";
147
+ const bytes = base64ToArrayBuffer(base64Data);
148
+ const extension = mimeType.split("/")[1] || "mp3";
149
+ return new File([bytes], `audio.${extension}`, { type: mimeType });
150
+ }
151
+ const bytes = base64ToArrayBuffer(audio);
152
+ return new File([bytes], "audio.mp3", { type: "audio/mpeg" });
153
+ }
154
+ throw new Error("Invalid audio input type");
155
+ }
156
+ ensureFileSupport() {
157
+ if (typeof File === "undefined") throw new Error("`File` is not available in this environment. Use Node.js 20 or newer, or pass a File object directly.");
158
+ }
159
+ };
160
+ /**
161
+ * Creates a Groq transcription adapter with an explicit API key.
162
+ * Type resolution happens here at the call site.
163
+ *
164
+ * @param model - The model name (e.g., 'whisper-large-v3-turbo')
165
+ * @param apiKey - Your Groq API key
166
+ * @param config - Optional additional configuration
167
+ * @returns Configured Groq transcription adapter instance
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * const adapter = createGroqTranscription('whisper-large-v3-turbo', 'gsk_...');
172
+ *
173
+ * const result = await generateTranscription({
174
+ * adapter,
175
+ * audio: audioFile,
176
+ * language: 'en',
177
+ * });
178
+ * ```
179
+ */
167
180
  function createGroqTranscription(model, apiKey, config) {
168
- return new GroqTranscriptionAdapter({ apiKey, ...config }, model);
181
+ return new GroqTranscriptionAdapter({
182
+ apiKey,
183
+ ...config
184
+ }, model);
169
185
  }
186
+ /**
187
+ * Creates a Groq transcription adapter using the `GROQ_API_KEY` environment
188
+ * variable. Type resolution happens here at the call site.
189
+ *
190
+ * Looks for `GROQ_API_KEY` in:
191
+ * - `process.env` (Node.js)
192
+ * - `window.env` (browser with injected env)
193
+ *
194
+ * @param model - The model name (e.g., 'whisper-large-v3-turbo')
195
+ * @param config - Optional configuration (excluding apiKey which is auto-detected)
196
+ * @returns Configured Groq transcription adapter instance
197
+ * @throws Error if GROQ_API_KEY is not found in environment
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const adapter = groqTranscription('whisper-large-v3-turbo');
202
+ *
203
+ * const result = await generateTranscription({
204
+ * adapter,
205
+ * audio: 'https://example.com/audio.mp3',
206
+ * });
207
+ *
208
+ * console.log(result.text)
209
+ * ```
210
+ */
170
211
  function groqTranscription(model, config) {
171
- const apiKey = getGroqApiKeyFromEnv();
172
- return createGroqTranscription(model, apiKey, config);
212
+ return createGroqTranscription(model, getGroqApiKeyFromEnv(), config);
173
213
  }
174
- export {
175
- GroqTranscriptionAdapter,
176
- createGroqTranscription,
177
- groqTranscription
178
- };
179
- //# sourceMappingURL=transcription.js.map
214
+ //#endregion
215
+ export { GroqTranscriptionAdapter, createGroqTranscription, groqTranscription };
216
+
217
+ //# sourceMappingURL=transcription.js.map