Skip to content
Open
25,466 changes: 16,831 additions & 8,635 deletions packages/typescript/ai-openrouter/src/model-meta.ts

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1,110 changes: 710 additions & 400 deletions scripts/openrouter.models.ts

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions testing/e2e/fixtures/image-gen/basic.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions testing/e2e/fixtures/transcription/basic.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"fixtures": [
{
"match": { "userMessage": "[transcription] transcribe the audio clip" },
"match": { "userMessage": "audio.mpeg" },
"response": {
"content": "Transcription result: I would like to buy a Fender Stratocaster please. The audio is clear with no background noise."
"transcription": {
"text": "I would like to buy a Fender Stratocaster please"
}
}
}
]
Expand Down
6 changes: 2 additions & 4 deletions testing/e2e/fixtures/tts/basic.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
{
"fixtures": [
{
"match": {
"userMessage": "[tts] generate speech for welcome to the guitar store"
},
"match": { "userMessage": "welcome to the guitar store" },
"response": {
"content": "Audio generated successfully. The text 'Welcome to the guitar store' has been converted to speech using a warm, professional voice."
"audio": "SGVsbG8gd29ybGQ="
}
}
]
Expand Down
13 changes: 13 additions & 0 deletions testing/e2e/fixtures/video-gen/basic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"fixtures": [
{
"match": { "userMessage": "a guitar being played in a store" },
"response": {
"video": {
"url": "https://example.com/guitar-store.mp4",
"duration": 10
}
}
}
]
}
35 changes: 34 additions & 1 deletion testing/e2e/global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,51 @@ import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

/**
* Directories to skip when loading JSON fixtures.
* - 'recorded' is for record-mode output
* - 'video-gen' uses programmatic registration (needs match.endpoint)
*/
const SKIP_FIXTURE_DIRS = new Set(['recorded', 'video-gen'])

export default async function globalSetup() {
const mock = new LLMock({ port: 4010, host: '127.0.0.1', logLevel: 'info' })

// Load all JSON fixture directories (except skipped ones)
const fixturesDir = path.resolve(__dirname, 'fixtures')
const entries = fs.readdirSync(fixturesDir, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'recorded') {
if (entry.isDirectory() && !SKIP_FIXTURE_DIRS.has(entry.name)) {
await mock.loadFixtureDir(path.join(fixturesDir, entry.name))
}
}

// Register media fixtures programmatically (require match.endpoint)
registerMediaFixtures(mock)

await mock.start()
console.log(`[aimock] started on port 4010`)
;(globalThis as any).__aimock = mock
}

function registerMediaFixtures(mock: LLMock) {
// Transcription: onTranscription sets match.endpoint = "transcription"
mock.onTranscription({
transcription: {
text: 'I would like to buy a Fender Stratocaster please',
},
})

// Video: onVideo sets match.endpoint = "video"
// id + status are required for the OpenAI SDK's videos API to work:
// - POST /v1/videos reads response.id for the job ID
// - GET /v1/videos/{id} reads response.status to determine completion
mock.onVideo('a guitar being played in a store', {
video: {
url: 'https://example.com/guitar-store.mp4',
duration: 10,
id: 'video-job-e2e',
status: 'completed',
},
})
}
104 changes: 104 additions & 0 deletions testing/e2e/src/components/ImageGenUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState } from 'react'
import {
useGenerateImage,
fetchServerSentEvents,
fetchHttpStream,
} from '@tanstack/ai-react'
import { generateImageFn } from '@/lib/server-functions'
import type { ImageGenerationResult } from '@tanstack/ai'
import type { Mode, Provider } from '@/lib/types'

interface ImageGenUIProps {
provider: Provider
mode: Mode
testId?: string
aimockPort?: number
}

export function ImageGenUI({
provider,
mode,
testId,
aimockPort,
}: ImageGenUIProps) {
const [prompt, setPrompt] = useState('')

const connectionOptions = () => {
const body = { provider, numberOfImages: 1, testId, aimockPort }

if (mode === 'sse') {
return { connection: fetchServerSentEvents('/api/image'), body }
}
if (mode === 'http-stream') {
return { connection: fetchHttpStream('/api/image/stream'), body }
}
return {
fetcher: async (input: { prompt: string }) => {
return generateImageFn({
data: {
prompt: input.prompt,
provider,
numberOfImages: 1,
aimockPort,
testId,
},
}) as Promise<ImageGenerationResult>
},
}
}

const { generate, result, isLoading, error, status } =
useGenerateImage(connectionOptions())

return (
<div className="p-4 space-y-4">
<div className="flex gap-2">
<input
data-testid="prompt-input"
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe the image..."
className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm"
/>
<button
data-testid="generate-button"
onClick={() => generate({ prompt })}
disabled={!prompt.trim() || isLoading}
className="px-4 py-2 bg-orange-500 text-white rounded text-sm font-medium disabled:opacity-50"
>
Generate
</button>
</div>
<div data-testid="generation-status">
{status === 'idle'
? 'idle'
: isLoading
? 'loading'
: error
? 'error'
: result
? 'complete'
: 'idle'}
</div>
{error && (
<div data-testid="generation-error" className="text-red-400 text-sm">
{error.message}
</div>
)}
{result && (
<div className="grid grid-cols-2 gap-4">
{result.images.map((img, i) => (
<img
key={i}
data-testid="generated-image"
src={img.url || `data:image/png;base64,${img.b64Json}`}
alt={`Generated ${i + 1}`}
className="rounded border border-gray-700"
/>
))}
</div>
)}
</div>
)
}
93 changes: 93 additions & 0 deletions testing/e2e/src/components/TTSUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useState } from 'react'
import {
useGenerateSpeech,
fetchServerSentEvents,
fetchHttpStream,
} from '@tanstack/ai-react'
import { generateSpeechFn } from '@/lib/server-functions'
import type { TTSResult } from '@tanstack/ai'
import type { Mode, Provider } from '@/lib/types'

interface TTSUIProps {
provider: Provider
mode: Mode
testId?: string
aimockPort?: number
}

export function TTSUI({ provider, mode, testId, aimockPort }: TTSUIProps) {
const [text, setText] = useState('')

const connectionOptions = () => {
const body = { provider, testId, aimockPort }

if (mode === 'sse') {
return { connection: fetchServerSentEvents('/api/tts'), body }
}
if (mode === 'http-stream') {
return { connection: fetchHttpStream('/api/tts/stream'), body }
}
return {
fetcher: async (input: { text: string; voice?: string }) => {
return generateSpeechFn({
data: {
text: input.text,
voice: input.voice,
provider,
aimockPort,
testId,
},
}) as Promise<TTSResult>
},
}
}

const { generate, result, isLoading, error, status } =
useGenerateSpeech(connectionOptions())

return (
<div className="p-4 space-y-4">
<div className="flex gap-2">
<input
data-testid="text-input"
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Text to speak..."
className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm"
/>
<button
data-testid="generate-button"
onClick={() => generate({ text })}
disabled={!text.trim() || isLoading}
className="px-4 py-2 bg-orange-500 text-white rounded text-sm font-medium disabled:opacity-50"
>
Generate
</button>
</div>
<div data-testid="generation-status">
{status === 'idle'
? 'idle'
: isLoading
? 'loading'
: error
? 'error'
: result
? 'complete'
: 'idle'}
</div>
{error && (
<div data-testid="generation-error" className="text-red-400 text-sm">
{error.message}
</div>
)}
{result && (
<audio
data-testid="generated-audio"
src={`data:audio/${result.format || 'mp3'};base64,${result.audio}`}
controls
/>
)}
</div>
)
}
Loading
Loading