diff --git a/.changeset/groq-summarize-adapter.md b/.changeset/groq-summarize-adapter.md new file mode 100644 index 000000000..87b163b13 --- /dev/null +++ b/.changeset/groq-summarize-adapter.md @@ -0,0 +1,9 @@ +--- +'@tanstack/ai-groq': minor +--- + +feat: add groqSummarize and createGroqSummarize adapters + +Groq now exposes tree-shakeable summarize factories that wrap `GroqTextAdapter` +in `ChatStreamSummarizeAdapter`, matching the pattern used by OpenAI, Anthropic, +Gemini, Ollama, Grok, and OpenRouter. diff --git a/docs/adapters/groq.md b/docs/adapters/groq.md index aef802040..b6fc64348 100644 --- a/docs/adapters/groq.md +++ b/docs/adapters/groq.md @@ -129,6 +129,24 @@ const stream = chat({ > If you previously passed `temperature` / `topP` / `maxTokens` at the root of `chat()`, see [Moving Sampling Options into modelOptions](../migration/sampling-options-to-model-options). +## Summarization + +Summarize long text content: + +```typescript +import { summarize } from "@tanstack/ai"; +import { groqSummarize } from "@tanstack/ai-groq"; + +const result = await summarize({ + adapter: groqSummarize("llama-3.3-70b-versatile"), + text: "Your long text to summarize...", + maxLength: 100, + style: "concise", // "concise" | "bullet-points" | "paragraph" +}); + +console.log(result.summary); +``` + ### Reasoning Enable reasoning for models that support it (e.g., `openai/gpt-oss-120b`, `qwen/qwen3-32b`). This allows the model to show its reasoning process, which is streamed as `thinking` chunks: @@ -202,6 +220,18 @@ Creates a Groq chat adapter with an explicit API key. **Returns:** A Groq chat adapter instance. +### `groqSummarize(model, config?)` + +Creates a Groq summarization adapter using environment variables. + +**Returns:** A Groq summarize adapter instance. + +### `createGroqSummarize(model, apiKey, config?)` + +Creates a Groq summarization adapter with an explicit API key. + +**Returns:** A Groq summarize adapter instance. + ## Limitations - **Text-to-Speech**: Groq does not currently expose a TTS adapter. Use OpenAI, Gemini, ElevenLabs, or fal for speech generation. diff --git a/docs/config.json b/docs/config.json index 72782240a..486b08ade 100644 --- a/docs/config.json +++ b/docs/config.json @@ -440,7 +440,8 @@ { "label": "Groq", "to": "adapters/groq", - "addedAt": "2026-04-15" + "addedAt": "2026-04-15", + "updatedAt": "2026-06-18" }, { "label": "ElevenLabs", diff --git a/package.json b/package.json index f821b130a..77df97b65 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "test:build": "nx affected --target=test:build --exclude=examples/**,testing/**", "test:types": "nx affected --targets=test:types --exclude=examples/**,testing/**", "test:knip": "knip", - "test:docs": "node scripts/verify-links.ts", + "test:docs": "tsx scripts/verify-links.ts", "test:kiira": "kiira check", "test:react-native": "pnpm --filter @tanstack/ai-react-native-smoke smoke", "test:e2e": "pnpm --filter @tanstack/ai-e2e test:e2e", @@ -82,4 +82,4 @@ "vite": "^7.3.3", "vitest": "^4.0.14" } -} +} \ No newline at end of file diff --git a/packages/ai-groq/README.md b/packages/ai-groq/README.md index 984b35e72..de7a6b080 100644 --- a/packages/ai-groq/README.md +++ b/packages/ai-groq/README.md @@ -72,6 +72,7 @@ const adapter = createGroqText('llama-3.3-70b-versatile', 'gsk_api_key') - ✅ Structured output (JSON Schema) - ✅ Function/tool calling - ✅ Multimodal input (text + images for vision models) +- ✅ Summarization (`groqSummarize`) - ❌ Embeddings (not supported by Groq) - ❌ Image generation (not supported by Groq) diff --git a/packages/ai-groq/src/adapters/summarize.ts b/packages/ai-groq/src/adapters/summarize.ts new file mode 100644 index 000000000..7ec5e44ca --- /dev/null +++ b/packages/ai-groq/src/adapters/summarize.ts @@ -0,0 +1,77 @@ +import { ChatStreamSummarizeAdapter } from '@tanstack/ai/adapters' +import { getGroqApiKeyFromEnv } from '../utils' +import { GroqTextAdapter } from './text' +import type { InferTextProviderOptions } from '@tanstack/ai/adapters' +import type { GROQ_CHAT_MODELS } from '../model-meta' +import type { GroqClientConfig } from '../utils' + +/** + * Configuration for Groq summarize adapter + */ +export interface GroqSummarizeConfig extends GroqClientConfig {} + +/** Model type for Groq summarization */ +export type GroqSummarizeModel = (typeof GROQ_CHAT_MODELS)[number] + +/** + * Creates a Groq summarize adapter with explicit API key. + * Type resolution happens here at the call site. + * + * @param model - The model name (e.g., 'llama-3.3-70b-versatile') + * @param apiKey - Your Groq API key + * @param config - Optional additional configuration + * @returns Configured Groq summarize adapter instance with resolved types + * + * @example + * ```typescript + * const adapter = createGroqSummarize('llama-3.3-70b-versatile', "gsk_..."); + * ``` + */ +export function createGroqSummarize( + model: TModel, + apiKey: string, + config?: Omit, +): ChatStreamSummarizeAdapter< + TModel, + InferTextProviderOptions> +> { + return new ChatStreamSummarizeAdapter( + new GroqTextAdapter({ apiKey, ...config }, model), + model, + 'groq', + ) +} + +/** + * Creates a Groq summarize adapter with automatic API key detection from environment variables. + * Type resolution happens here at the call site. + * + * Looks for `GROQ_API_KEY` in: + * - `process.env` (Node.js) + * - `window.env` (Browser with injected env) + * + * @param model - The model name (e.g., 'llama-3.3-70b-versatile') + * @param config - Optional configuration (excluding apiKey which is auto-detected) + * @returns Configured Groq summarize adapter instance with resolved types + * @throws Error if GROQ_API_KEY is not found in environment + * + * @example + * ```typescript + * // Automatically uses GROQ_API_KEY from environment + * const adapter = groqSummarize('llama-3.3-70b-versatile'); + * + * await summarize({ + * adapter, + * text: "Long article text..." + * }); + * ``` + */ +export function groqSummarize( + model: TModel, + config?: Omit, +): ChatStreamSummarizeAdapter< + TModel, + InferTextProviderOptions> +> { + return createGroqSummarize(model, getGroqApiKeyFromEnv(), config) +} diff --git a/packages/ai-groq/src/index.ts b/packages/ai-groq/src/index.ts index 034ff38d0..091052af6 100644 --- a/packages/ai-groq/src/index.ts +++ b/packages/ai-groq/src/index.ts @@ -14,6 +14,14 @@ export { type GroqTextProviderOptions, } from './adapters/text' +// Summarize - thin factory functions over @tanstack/ai's ChatStreamSummarizeAdapter +export { + createGroqSummarize, + groqSummarize, + type GroqSummarizeConfig, + type GroqSummarizeModel, +} from './adapters/summarize' + // Types export type { GroqChatModelProviderOptionsByName, diff --git a/packages/ai-groq/tests/groq-adapter.test.ts b/packages/ai-groq/tests/groq-adapter.test.ts index 0058d2652..6590bc83a 100644 --- a/packages/ai-groq/tests/groq-adapter.test.ts +++ b/packages/ai-groq/tests/groq-adapter.test.ts @@ -12,6 +12,10 @@ import { createGroqText as _realCreateGroqText, groqText as _realGroqText, } from '../src/adapters/text' +import { + createGroqSummarize, + groqSummarize, +} from '../src/adapters/summarize' import { EventType } from '@tanstack/ai' import type { StreamChunk, Tool } from '@tanstack/ai' import type { GroqTextProviderOptions } from '../src/index' @@ -188,6 +192,37 @@ describe('Groq adapters', () => { }) }) }) + + describe('Summarize adapter', () => { + it('creates a summarize adapter with explicit API key', () => { + const adapter = createGroqSummarize( + 'llama-3.3-70b-versatile', + 'test-api-key', + ) + + expect(adapter).toBeDefined() + expect(adapter.kind).toBe('summarize') + expect(adapter.name).toBe('groq') + expect(adapter.model).toBe('llama-3.3-70b-versatile') + }) + + it('creates a summarize adapter from environment variable', () => { + vi.stubEnv('GROQ_API_KEY', 'env-api-key') + + const adapter = groqSummarize('llama-3.3-70b-versatile') + + expect(adapter).toBeDefined() + expect(adapter.kind).toBe('summarize') + }) + + it('throws if GROQ_API_KEY is not set when using groqSummarize', () => { + vi.stubEnv('GROQ_API_KEY', '') + + expect(() => groqSummarize('llama-3.3-70b-versatile')).toThrow( + 'GROQ_API_KEY is required', + ) + }) + }) }) describe('Groq AG-UI event emission', () => { diff --git a/packages/ai/tests/summarize-max-length.test.ts b/packages/ai/tests/summarize-max-length.test.ts index 20b95a823..b6e577906 100644 --- a/packages/ai/tests/summarize-max-length.test.ts +++ b/packages/ai/tests/summarize-max-length.test.ts @@ -75,6 +75,26 @@ describe('ChatStreamSummarizeAdapter — maxLength reaches the wrapped adapter u expect(opts?.['maxTokens']).toBeUndefined() }) + it('Groq adapter receives maxLength as max_completion_tokens', async () => { + const { textAdapter, lastModelOptions } = createRecordingTextAdapter() + const adapter = new ChatStreamSummarizeAdapter( + textAdapter, + 'llama-3.3-70b-versatile', + 'groq', + ) + + await adapter.summarize({ + model: 'llama-3.3-70b-versatile', + text: 'hi', + maxLength: 256, + logger, + }) + + const opts = lastModelOptions() + expect(opts?.['max_completion_tokens']).toBe(256) + expect(opts?.['maxTokens']).toBeUndefined() + }) + it('Ollama adapter receives maxLength AND the temperature default nested under options', async () => { const { textAdapter, lastModelOptions } = createRecordingTextAdapter() const adapter = new ChatStreamSummarizeAdapter( diff --git a/testing/e2e/src/lib/feature-support.ts b/testing/e2e/src/lib/feature-support.ts index 5e722de63..96ce4feb7 100644 --- a/testing/e2e/src/lib/feature-support.ts +++ b/testing/e2e/src/lib/feature-support.ts @@ -165,6 +165,7 @@ export const matrix: Record> = { 'anthropic', 'gemini', 'ollama', + 'groq', 'grok', 'openrouter', ]), @@ -173,6 +174,7 @@ export const matrix: Record> = { 'anthropic', 'gemini', 'ollama', + 'groq', 'grok', 'openrouter', ]), diff --git a/testing/e2e/src/routes/api.summarize.ts b/testing/e2e/src/routes/api.summarize.ts index 6ba0e2e37..c8aec9e4e 100644 --- a/testing/e2e/src/routes/api.summarize.ts +++ b/testing/e2e/src/routes/api.summarize.ts @@ -4,6 +4,7 @@ import { createOpenaiSummarize } from '@tanstack/ai-openai' import { createAnthropicSummarize } from '@tanstack/ai-anthropic' import { createGeminiSummarize } from '@tanstack/ai-gemini' import { createOllamaSummarize } from '@tanstack/ai-ollama' +import { createGroqSummarize } from '@tanstack/ai-groq' import { createGrokSummarize } from '@tanstack/ai-grok' import { createOpenRouterSummarize } from '@tanstack/ai-openrouter' import type { Provider } from '@/lib/types' @@ -46,6 +47,11 @@ function createSummarizeAdapter( httpOptions: { baseUrl: llmockBase(aimockPort), headers }, }), ollama: () => createOllamaSummarize('mistral', llmockBase(aimockPort)), + groq: () => + createGroqSummarize('llama-3.3-70b-versatile', DUMMY_KEY, { + baseURL: llmockBase(aimockPort), + defaultHeaders: headers, + }), grok: () => createGrokSummarize('grok-build-0.1', DUMMY_KEY, { baseURL: openaiUrl(aimockPort),