Skip to content
Merged
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
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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