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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ LLM Scraper is a TypeScript library that allows you to extract structured data f

### Features

- Supports GPT, Sonnet, Gemini, Llama, Qwen model series
- Supports GPT, Sonnet, Gemini, Llama, Qwen, MiniMax model series
- Schemas defined with Zod or JSON Schema
- Full type-safety with TypeScript
- Based on Playwright framework
Expand Down Expand Up @@ -91,6 +91,23 @@ LLM Scraper is a TypeScript library that allows you to extract structured data f
const llm = groq('llama3-8b-8192')
```

**MiniMax**

```
npm i @ai-sdk/openai
```

```js
import { createOpenAI } from '@ai-sdk/openai'
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: process.env.MINIMAX_API_KEY,
compatibility: 'compatible',
})

const llm = minimax.chat('MiniMax-M3')
```

**Ollama**

```
Expand Down
104 changes: 104 additions & 0 deletions examples/minimax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { chromium } from 'playwright'
import { Output, wrapLanguageModel } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import { z } from 'zod'
import LLMScraper from './../src/index.js'

// Initialize MiniMax provider using OpenAI-compatible API
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: process.env.MINIMAX_API_KEY,
compatibility: 'compatible',
})

// MiniMax uses the OpenAI-compatible API but does not support `json_schema`
// response format. Use middleware to:
// 1. Downgrade json_schema to json_object (MiniMax doesn't support json_schema)
// 2. Embed the JSON schema in the system prompt so the model knows the format
// 3. Strip any <think>...</think> tags from responses (harmless if absent)
const llm = wrapLanguageModel({
model: minimax.chat('MiniMax-M3'),
middleware: {
transformParams: async ({ params }) => {
if (params.responseFormat?.type === 'json' && params.responseFormat?.schema) {
const schemaStr = JSON.stringify(params.responseFormat.schema)
const jsonInstruction = `\n\nYou MUST respond with ONLY valid JSON matching this schema: ${schemaStr}`
const newPrompt = Array.isArray(params.prompt)
? params.prompt.map((msg: any) =>
msg.role === 'system' && typeof msg.content === 'string'
? { ...msg, content: msg.content + jsonInstruction }
: msg
)
: params.prompt
return { ...params, responseFormat: { type: 'json' }, prompt: newPrompt }
}
return params
},
wrapGenerate: async ({ doGenerate }) => {
const result = await doGenerate()
return {
...result,
content: result.content?.map((part: any) =>
part.type === 'text'
? { ...part, text: part.text?.replace(/<think>[\s\S]*?<\/think>\s*/g, '') }
: part
),
}
},
wrapStream: async ({ doStream }) => {
const { stream, ...rest } = await doStream()
let thinking = false
return {
stream: stream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
if (chunk.type === 'text-delta') {
let text = (chunk as any).delta || chunk.textDelta || ''
if (text.includes('<think>')) thinking = true
if (thinking) {
const endIdx = text.indexOf('</think>')
if (endIdx !== -1) {
thinking = false
text = text.slice(endIdx + '</think>'.length)
} else {
return
}
}
if (text)
controller.enqueue({ ...chunk, delta: text, textDelta: text })
} else {
controller.enqueue(chunk)
}
},
})
),
...rest,
}
},
},
})

// Launch a browser instance
const browser = await chromium.launch()

// Initialize a new LLMScraper with MiniMax model
const scraper = new LLMScraper(llm)

// Open the page
const page = await browser.newPage()
await page.goto('https://example.com')

// Define schema to extract contents into
const schema = z.object({
h1: z.string().describe('The main heading of the page'),
})

// Run the scraper
const { data } = await scraper.run(page, Output.object({ schema }), {
format: 'html',
})

console.log(data)

await page.close()
await browser.close()
159 changes: 159 additions & 0 deletions tests/minimax-integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { createOpenAI } from '@ai-sdk/openai'
import { wrapLanguageModel } from 'ai'
import { z } from 'zod'
import { Output } from 'ai'
import { chromium, Browser } from 'playwright'
import LLMScraper from '../src/index.js'

const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY

// Middleware for MiniMax:
// 1. Downgrade json_schema to json_object (MiniMax doesn't support json_schema)
// 2. Embed schema in system prompt so the model knows the expected format
// 3. Strip any <think>...</think> tags from responses (harmless if absent)
function createMiniMaxModel(modelId: string, apiKey: string) {
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey,
compatibility: 'compatible',
})

return wrapLanguageModel({
model: minimax.chat(modelId),
middleware: {
transformParams: async ({ params }) => {
if (params.responseFormat?.type === 'json' && params.responseFormat?.schema) {
const schemaStr = JSON.stringify(params.responseFormat.schema)
const jsonInstruction = `\n\nYou MUST respond with ONLY valid JSON matching this schema: ${schemaStr}`
const newPrompt = Array.isArray(params.prompt)
? params.prompt.map((msg: any) =>
msg.role === 'system' && typeof msg.content === 'string'
? { ...msg, content: msg.content + jsonInstruction }
: msg
)
: params.prompt
return { ...params, responseFormat: { type: 'json' }, prompt: newPrompt }
}
return params
},
wrapGenerate: async ({ doGenerate }) => {
const result = await doGenerate()
return {
...result,
content: result.content?.map((part: any) =>
part.type === 'text'
? { ...part, text: part.text?.replace(/<think>[\s\S]*?<\/think>\s*/g, '') }
: part
),
}
},
wrapStream: async ({ doStream }) => {
const { stream, ...rest } = await doStream()
let thinking = false
return {
stream: stream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
if (chunk.type === 'text-delta') {
let text = (chunk as any).delta || chunk.textDelta || ''
if (text.includes('<think>')) thinking = true
if (thinking) {
const endIdx = text.indexOf('</think>')
if (endIdx !== -1) {
thinking = false
text = text.slice(endIdx + '</think>'.length)
} else {
return
}
}
if (text)
controller.enqueue({ ...chunk, delta: text, textDelta: text })
} else {
controller.enqueue(chunk)
}
},
})
),
...rest,
}
},
},
})
}

const describeIntegration = MINIMAX_API_KEY ? describe : describe.skip

describeIntegration('MiniMax integration tests', () => {
let browser: Browser
let scraper: LLMScraper

beforeAll(async () => {
browser = await chromium.launch({ channel: 'chrome' })
scraper = new LLMScraper(createMiniMaxModel('MiniMax-M3', MINIMAX_API_KEY!))
}, 30000)

afterAll(async () => {
if (browser) {
await browser.close()
}
})

it('scrapes example.com heading with MiniMax', async () => {
const page = await browser.newPage()
await page.goto('https://example.com')

const schema = z.object({
h1: z.string().describe('The main heading of the page'),
})

const { data } = await scraper.run(page, Output.object({ schema }), {
format: 'html',
})

expect(data).toBeDefined()
expect(data.h1).toBe('Example Domain')

await page.close()
}, 60000)

it('scrapes example.com with streaming using MiniMax', async () => {
const page = await browser.newPage()
await page.goto('https://example.com')

const schema = z.object({
h1: z.string().describe('The main heading of the page'),
})

const { stream } = await scraper.stream(page, Output.object({ schema }))

let text = ''
for await (const item of stream) {
text = item.h1 || ''
}

expect(text).toBe('Example Domain')

await page.close()
}, 60000)

it('scrapes example.com with markdown format using MiniMax', async () => {
const page = await browser.newPage()
await page.goto('https://example.com')

const schema = z.object({
title: z.string().describe('The title or main heading of the page'),
description: z.string().describe('A brief description found on the page'),
})

const { data } = await scraper.run(page, Output.object({ schema }), {
format: 'markdown',
})

expect(data).toBeDefined()
expect(data.title).toBeTruthy()
expect(data.description).toBeTruthy()

await page.close()
}, 60000)
})
99 changes: 99 additions & 0 deletions tests/minimax.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, it, expect } from 'vitest'
import { createOpenAI } from '@ai-sdk/openai'

describe('MiniMax provider configuration', () => {
it('creates a MiniMax provider with correct baseURL and compatibility mode', () => {
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: 'test-key',
compatibility: 'compatible',
})

expect(minimax).toBeDefined()
expect(typeof minimax).toBe('function')
})

it('creates a chat model instance for MiniMax-M3', () => {
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: 'test-key',
compatibility: 'compatible',
})

const model = minimax.chat('MiniMax-M3')
expect(model).toBeDefined()
expect(model.modelId).toBe('MiniMax-M3')
})

it('creates a chat model instance for MiniMax-M2.7', () => {
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: 'test-key',
compatibility: 'compatible',
})

const model = minimax.chat('MiniMax-M2.7')
expect(model).toBeDefined()
expect(model.modelId).toBe('MiniMax-M2.7')
})

it('creates a chat model instance for MiniMax-M2.7-highspeed', () => {
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: 'test-key',
compatibility: 'compatible',
})

const model = minimax.chat('MiniMax-M2.7-highspeed')
expect(model).toBeDefined()
expect(model.modelId).toBe('MiniMax-M2.7-highspeed')
})

it('uses MINIMAX_API_KEY from environment', () => {
const originalKey = process.env.MINIMAX_API_KEY
process.env.MINIMAX_API_KEY = 'env-test-key'

const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: process.env.MINIMAX_API_KEY,
compatibility: 'compatible',
})

const model = minimax.chat('MiniMax-M3')
expect(model).toBeDefined()

if (originalKey !== undefined) {
process.env.MINIMAX_API_KEY = originalKey
} else {
delete process.env.MINIMAX_API_KEY
}
})
})

describe('MiniMax provider with LLMScraper', () => {
it('initializes LLMScraper with MiniMax model', async () => {
const { default: LLMScraper } = await import('../src/index.js')
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: 'test-key',
compatibility: 'compatible',
})

const scraper = new LLMScraper(minimax.chat('MiniMax-M3'))
expect(scraper).toBeDefined()
expect(scraper).toBeInstanceOf(LLMScraper)
})

it('initializes LLMScraper with MiniMax-M2.7-highspeed', async () => {
const { default: LLMScraper } = await import('../src/index.js')
const minimax = createOpenAI({
baseURL: 'https://api.minimax.io/v1',
apiKey: 'test-key',
compatibility: 'compatible',
})

const scraper = new LLMScraper(minimax.chat('MiniMax-M2.7-highspeed'))
expect(scraper).toBeDefined()
expect(scraper).toBeInstanceOf(LLMScraper)
})
})