Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/groq-summarize-adapter.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 30 additions & 0 deletions docs/adapters/groq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,8 @@
{
"label": "Groq",
"to": "adapters/groq",
"addedAt": "2026-04-15"
"addedAt": "2026-04-15",
"updatedAt": "2026-06-18"
Comment on lines +443 to +444

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Refresh the Groq entry’s updatedAt.

2026-06-18 looks stale for this docs change; please bump it to 2026-06-25 so the navigation metadata stays accurate.

As per coding guidelines, docs/config.json entries must update updatedAt when making content changes.

🛠️ Suggested fix
-          "updatedAt": "2026-06-18"
+          "updatedAt": "2026-06-25"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"addedAt": "2026-04-15",
"updatedAt": "2026-06-18"
"addedAt": "2026-04-15",
"updatedAt": "2026-06-25"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/config.json` around lines 443 - 444, The Groq entry’s metadata is stale:
update the updatedAt value in docs/config.json for the affected entry from the
older date to the current docs change date. Use the existing entry fields around
addedAt/updatedAt to locate the record, and keep the updatedAt timestamp aligned
with the content change while leaving the rest of the metadata unchanged.

Source: Coding guidelines

},
{
"label": "ElevenLabs",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -82,4 +82,4 @@
"vite": "^7.3.3",
"vitest": "^4.0.14"
}
}
}
1 change: 1 addition & 0 deletions packages/ai-groq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
77 changes: 77 additions & 0 deletions packages/ai-groq/src/adapters/summarize.ts
Original file line number Diff line number Diff line change
@@ -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<TModel extends GroqSummarizeModel>(
model: TModel,
apiKey: string,
config?: Omit<GroqSummarizeConfig, 'apiKey'>,
): ChatStreamSummarizeAdapter<
TModel,
InferTextProviderOptions<GroqTextAdapter<TModel>>
> {
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<TModel extends GroqSummarizeModel>(
model: TModel,
config?: Omit<GroqSummarizeConfig, 'apiKey'>,
): ChatStreamSummarizeAdapter<
TModel,
InferTextProviderOptions<GroqTextAdapter<TModel>>
> {
return createGroqSummarize(model, getGroqApiKeyFromEnv(), config)
}
8 changes: 8 additions & 0 deletions packages/ai-groq/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
35 changes: 35 additions & 0 deletions packages/ai-groq/tests/groq-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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', () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/ai/tests/summarize-max-length.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions testing/e2e/src/lib/feature-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'anthropic',
'gemini',
'ollama',
'groq',
'grok',
'openrouter',
]),
Expand All @@ -173,6 +174,7 @@ export const matrix: Record<Feature, Set<Provider>> = {
'anthropic',
'gemini',
'ollama',
'groq',
'grok',
'openrouter',
]),
Expand Down
6 changes: 6 additions & 0 deletions testing/e2e/src/routes/api.summarize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
}),
Comment on lines +50 to +54

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how GroqTextAdapter builds its request URL from baseURL/config
fd -t f . packages/ai-groq/src -e ts | xargs rg -nC3 'baseURL|/v1|chat/completions|new URL|fetch\('
# Compare with grok adapter URL construction
fd -t f . packages/ai-grok/src -e ts | xargs rg -nC3 'baseURL|/v1|chat/completions'

Repository: TanStack/ai

Length of output: 20493


Ensure Groq e2e tests target the /openai/v1 endpoint path

The Groq adapter expects endpoints under /openai/v1, distinct from the standard /v1 used by OpenAI and Grok. The withGroqDefaults utility only injects the correct default if baseURL is absent; since the e2e test explicitly passes baseURL: llmockBase(aimockPort), the adapter uses that root path directly, resulting in requests to /chat/completions instead of the required /openai/v1/chat/completions.

Update the configuration to append the Groq-specific path:

// testing/e2e/src/routes/api.summarize.ts
groq: () =>
  createGroqSummarize('llama-3.3-70b-versatile', DUMMY_KEY, {
    baseURL: `${llmockBase(aimockPort)}/openai/v1`,
    defaultHeaders: headers,
  }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/e2e/src/routes/api.summarize.ts` around lines 50 - 54, The Groq e2e
setup is overriding the adapter’s default endpoint and missing the required
/openai/v1 path, so requests are going to the wrong route. Update the groq
configuration in api.summarize.ts so createGroqSummarize still uses
llmockBase(aimockPort) but with the Groq-specific /openai/v1 suffix, ensuring
the adapter targets /openai/v1/chat/completions instead of the root
/chat/completions path.

grok: () =>
createGrokSummarize('grok-build-0.1', DUMMY_KEY, {
baseURL: openaiUrl(aimockPort),
Expand Down